无源NFC墨水屏制作


1.前言

之前在网上看到一个开源项目:https://oshwhub.com/ludas/nfc-epd-driver
觉得很有意思,想复刻一下,但是复刻失败了,因为我不清楚其中原理,也没有源码可供调试,所有花时间好好研究了一下。

无源NFC墨水屏的意思就是不需要供电或者内置电池,利用NFC的感应磁场来进行供电,然后通过程序控制对墨水屏幕进行刷新,得益于墨水屏掉电依然可以显示内容的特性,就可以实现一个完全不用电源并且可以被刷新的屏幕了。

先放成品
视频演示:https://www.bilibili.com/video/BV1rWitBTEdJ/


2. 分析

原项目采用 nxp芯片 NT3H1101W0FHKH,  这个芯片具有能量收集的功能(感应到NFC场,输出3v3),主控mcu 使用的是低功耗 意法半导体的 STM32L011D4P6。
在刷新墨水屏屏幕时, NT3H1101W0FHKH 接收到数据,并给主控mcu供电,通过 I2C协议 与主控mcu传递数据,然后主控mcu在此同时对墨水屏 进行刷新。

所有首先我要搞明白 NT3H1101W0FHKH 这颗芯片的使用

3. NT3H1101

这里我简单画了个测试版:


板子很简单,I2C总线和FD引脚上加了上拉电阻,然后加了去耦电容,还有一个谐振匹配的电容(这个加不加都能识别到)。
关键在于引出所有 NT3H1101 引脚 (I2C通信引脚,FD场检测引脚,能量回收供电引脚),这样方便我后续测试。

与我熟悉的 主控mcu进行数据交互,通过简单的I2C地址扫描,找到了 NT3H1101 I2C通信地址:

 复制代码 隐藏代码
voidsetup(){
  Wire.begin();
  Serial.begin(9600);
  Serial.println("\nI2C Scanner");
}
voidloop(){
  byte error, address;
int nDevices;
  Serial.println("Scanning...");
  nDevices = 0;
for(address = 1; address < 127; address++ ) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0) {
      Serial.print("I2C device found at address 0x");
      if (address<16) Serial.print("0");
      Serial.println(address, HEX);
      nDevices++;
    }
  }
if (nDevices == 0) Serial.println("No I2C devices found\n");
delay(5000);
}

那么剩下的主控MCU 与 NT3H1101 进行数据交互,通过阅读数据手册,得知,NT3H1101 与 mcu进行数据交互时,进入了 passthrough模式, 这个模式前提是需要 NT3H1101 有外部供电,一旦掉电后 就会退出 passthrough模式(这里可以用 NT3H1101自己回收的能量给自己供电),除此还需要对 NT3H1101 会话寄存器进行一些设置才能开启passthrough模式。

另外这里说一下 俩个概念:setting register 和 session register,EEPROM
setting register 简单可以理解成掉电不丢失,在每次por后数据依然存在。
session register 简单可以理解成掉电丢失,每次por后丢失,记录芯片的一些状态信息及临时配置。
EEPROM 是掉电不丢失的,有读写寿命,就像对NFC卡片写入个URL,或者打开APP这类数据都写入到这部分空间,本次不会用到这部分空间。

每一次上电,session register 的一部分配置选项会从 setting register 中载入,而我们要修改的 passthrough模式,只存在 session register中的(因为只有存在供电的情况才会使用到passthrough),所以每一次上电后主控mcu都需要通过I2C总线对 NT3H1101 的passthrough模式进行设置,才可以进行数据传输。


要修改session register 在 NT3H1101 中 I2C的访问地址是0xFE。
在I2C接口下,一个地址都是对应16字节数据,只不过在session register中只有前7个字节才有用,每个字节都对应一个寄存器名字:


相当于一个寄存器占了一个字节,一个字节中的8位,每一位(Filed)对应不同功能,比如我要修改的passthrough模式,是在NC_REG寄存器中:


他在NC_REG寄存器的第六位(PTHRU_ON_OFF),代码实现对第六位寄存器写操作:

 复制代码 隐藏代码
voidstartPassthrough(){
  Wire.beginTransmission(NT3H_I2C_ADDR);
  Wire.write(0xFE);  //会话寄存器的 起始地址
  Wire.write(0x00);  //第一个session register 即 NC_REG
  Wire.write(0x40); //发送掩码,告诉要修改这个寄存器的第几位
  Wire.write(0x40);   // 将其对应位修改为1
  Wire.endTransmission();
}

这里掩码操作是必须的,这个是NT3H1101 寄存器写入的规则,防止在喂入一个字节数据的时候修改到其他位。

开启了passthrogh模式后,我们需要知道RF接口到 I2C接口数据写入到了哪个地址,以及主控MCU什么时间开始读取对应地址的数据:


这里提供了俩种方式:

  • 轮询  NS_REG寄存器的 SRAM_I2C_READY位。
    2.利用FD引脚,在settings register中配置功能(可以配置场出现,数据到达等事件改变FD引脚电平状态),然后主控mcu使用中断机制来获悉数据到达。
  • 这里我采用 轮询寄存器方式,那么需要对寄存器进行读取:


     复制代码 隐藏代码
    boolean checkReady(){
        byte value;
        Wire.beginTransmission(NT3H_I2C_ADDR);
        Wire.write(0xFE); //会话寄存器的 起始地址
        Wire.write(0x06); //选中 NC_REG寄存器
        Wire.endTransmission(); 
        Wire.requestFrom(NT3H_I2C_ADDR,1);
        if (Wire.available() == 1) {
          value=Wire.read();
        }
        return value & 0b10000// 判断NS_REG寄存器的第4位,即SRAM_I2C_READY
    }

    OK,到此俩个寄存器读写已经搞好了,剩下就是读取一个地址,将RF-> I2C的数据读取出来,NT3H1101 在有外部供电的情况下,SRAM空间会映射到I2C地址 0xF8 - 0xFB 页,每个页有16个字节,所有SRAM一共有 64字节。

     复制代码 隐藏代码
    voidReadDataBlock(const byte block_address, uint8_t *out_buffer, int out_buffer_length)
    {
          Wire.beginTransmission(NT3H_I2C_ADDR);
          Wire.write(block_address);
          Wire.endTransmission();
          Wire.requestFrom(NT3H_I2C_ADDR, out_buffer_length);
          if (Wire.available() == out_buffer_length) {
            for (int i = 0; i < out_buffer_length; i++)
            {
              out_buffer[i+16*(block_address-0xf8)] = Wire.read();
            }
          }
    }

    voidreadPages(uint8_t startPage, uint8_t endPage, uint8_t *data64){
        for (uint8_t page = startPage; page <= endPage; page++) {
            ReadDataBlock(page, data64,16)
        }
    }

    voidloop(){
            startPassthrough();
            if (checkReady()){
              byte data64[64] = {0};
              readPages(0xf8,0xfb, data64);                
              for (size_t i = 0; i < 64; i++)
              {
                printHex(data64[i]);
              }
            }        
    }

    代码很简单,每次将从RF接口读取的数据打印出来。
    到此,基本的通信RF -> I2C 数据传递算是已经通过了。

    4. APP端的大数据传输失败问题

    这里说一下我手机上用的APP是:NXP I2C Demo


    这个APP提供了NXP官方提供开发板的一些demo 设置,以及对NDFF的读写 (前面提到的对EEPROM的读写 ),这里我主要利用对固件烧录功能,这部分功能就是通过passthrough模式进行 RF-> I2C数据传输,测试程序也是通过这部分功能进行测试的。

    但在实际测试过程中我发现对大文件的传输,必然失败,并且是处于一个固定大小。很明显这种不是偶发问题,偶发问题不会在固定大小是失败。

    经过排查了logcat 日志:

     复制代码 隐藏代码
    $ adb logcat |grep FLASH
    01-05 20:19:51.661 32438 20705 D FLASH   : Flashing to start
    01-05 20:19:51.899 32438 20705 D FLASH   : Start Block write 1 out of 3
    01-05 20:19:51.999 32438 20705 D FLASH   : Starting Block writing
    01-05 20:20:20.316 32438 20705 D FLASH   : All Blocks written
    01-05 20:20:21.317 32438 20705 D FLASH   : Wait finished
    01-05 20:20:21.353 32438 20705 D FLASH   : Block read
    01-05 20:20:21.353 32438 20705 D FLASH   : was nak

    通过日志看到,这个app对于大文件进行了切片,最后到 was nak就会fail, 所有我找了一下这个app的源码,研究一下这个app内部逻辑,好在找到nxp官方提供了app 源码包(省下我逆向app的时间了),虽然版本是低版本,但是逻辑没变化,寻找关键日志:

     复制代码 隐藏代码
    wshuo@wshuo-desktop:~/Downloads/SW3648/NTAG_I2C_Demo_AndroidApp$ grep "was nak" -r .
    grep: ./build/intermediates/transforms/dex/debug/folders/1000/1f/main/classes.dex: 匹配到二进制文件
    grep: ./build/intermediates/classes/debug/com/nxp/nfc_demo/reader/Ntag_I2C_Demo.class: 匹配到二进制文件
    ./src/com/nxp/nfc_demo/reader/Ntag_I2C_Demo.java:                   Log.d("FLASH""was nak");

    Ntag_I2C_Demo.java:

     复制代码 隐藏代码
        public Boolean Flash(byte[] bytesToFlash){
            int sectorSize = PAGE_SIZE;  //4096

            byte[] data = null;
            byte[] flashData = null;

            try {
                int length = bytesToFlash.length;  //总长度
                int flashes = length / sectorSize + (length % sectorSize == 0 ? 0 : 1);
                int blocks = (int) Math.ceil(length / (float) reader.getSRAMSize());

                // Set the number of writings
                FlashMemoryActivity.setFLashDialogMax(blocks);

                for (int i = 0; i < flashes; i++) {
                    int flash_addr = 0x4000 + i * sectorSize;
                    int flash_length = 0;

                    if (length - (i + 1) * sectorSize < 0) {
                        flash_length = roundUp(length % sectorSize);
                        flashData = new byte[flash_length];
                        Arrays.fill(flashData, (byte) 0);
                        System.arraycopy(bytesToFlash, i * sectorSize, flashData, 0, length % sectorSize);
                    } else {
                        flash_length = sectorSize;
                        flashData = new byte[flash_length];
                        System.arraycopy(bytesToFlash, i * sectorSize, flashData, 0, sectorSize);
                    }

                    data = new byte[reader.getSRAMSize()];
                    data[reader.getSRAMSize() - 4] = 'F';
                    data[reader.getSRAMSize() - 3] = 'P';

                    data[reader.getSRAMSize() - 8] = (byte) (flash_length >> 24 & 0xFF);
                    data[reader.getSRAMSize() - 7] = (byte) (flash_length >> 16 & 0xFF);
                    data[reader.getSRAMSize() - 6] = (byte) (flash_length >> 8 & 0xFF);
                    data[reader.getSRAMSize() - 5] = (byte) (flash_length & 0xFF);

                    data[reader.getSRAMSize() - 12] = (byte) (flash_addr >> 24 & 0xFF);
                    data[reader.getSRAMSize() - 11] = (byte) (flash_addr >> 16 & 0xFF);
                    data[reader.getSRAMSize() - 10] = (byte) (flash_addr >> 8 & 0xFF);
                    data[reader.getSRAMSize() - 9] = (byte) (flash_addr & 0xFF);

                    Log.d("FLASH""Flashing to start");
                    reader.writeSRAMBlock(data, null);
                    Log.d("FLASH""Start Block write " + (i + 1) + " out of " + flashes);

                    reader.waitforI2Cread(100);

                    Log.d("FLASH""Starting Block writing");
                    reader.writeSRAM(flashData, R_W_Methods.Fast_Mode, this);
                    Log.d("FLASH""All Blocks written");

                    reader.waitforI2Cwrite(500);
                    Thread.sleep(500);

                    Log.d("FLASH""Wait finished");
                    byte[] response = reader.readSRAMBlock();
                    Log.d("FLASH""Block read");

                    if (response[reader.getSRAMSize() - 4] != 'A' || response[reader.getSRAMSize() - 3] != 'C' || response[reader.getSRAMSize() - 2] != 'K') {
                        Log.d("FLASH""was nak");
                        returnfalse;
                    }
                    Log.d("FLASH""was ack");
                }
                Log.d("FLASH""Flash completed");

                data = new byte[reader.getSRAMSize()];
                data[reader.getSRAMSize() - 4] = 'F';
                data[reader.getSRAMSize() - 3] = 'S';
                reader.writeSRAMBlock(data, null);

                // Wait for the I2C to be ready
                reader.waitforI2Cread(DELAY_TIME);
                returntrue;

    可以看到这个app在发送文件开始会发送一个启始头:这个头的 flash_length 为烧录固件长度,flash_addr 为烧录地址(这部分信息对我们这个项目没用),每一切片结束后需要I2C给 RF一个 ACK 表示,才会继续下一个切片。
    所有这里我需要解决俩个问题:

  • 剔除无效数据,flash_length flash_addr数据,方案:利用关键标识符号 FP 以及 前 28 字节都为数据0(如果所有数据当做有效数据会对墨水屏显示造成干扰)。
  • 在每一切片结束后,构造ACK数据返回给 APP。
  • 关于第一点很好解决,简单逻辑判断即可,我在第二点问题上解决了很久,原因就在于 RF在往里写数据,I2C也在往里写数据,这其中有个同步的问题。

    我尝试过一直通过I2C写入ACK数据,发现只有在I2C读取完整个SRAM空间前写入有效,但是这样就破坏了RF写入的数据。
    无奈只能寻找NXP官方单片机的代码参考(看ACK是什么时机写入的):

     复制代码 隐藏代码
    wshuo@wshuo-desktop:~/Downloads/SW3647/workspace_ntag_i2c_plus$ grep "'A'" -r .
    ./NTAG_I2C_Explorer_BootLoader/src/main.c:      sram_buf[NFC_MEM_SRAM_SIZE - 3] = 'A';
    ./NTAG_I2C_Explorer_BootLoader/src/main.c:      sram_buf[NFC_MEM_SRAM_SIZE - 4] = 'A';
    ./NTAG_I2C_Explorer_BootLoader/src/hid_desc.c:  'A', 0,
    wshuo@wshuo-desktop:~/Downloads/SW3647/workspace_ntag_i2c_plus$ subl ./NTAG_I2C_Explorer_BootLoader/src/main.c

    我发现一条比较重要的信息:

     复制代码 隐藏代码
        if (flash((void*) addresse, (void*) data, size)) {
            HW_switchLEDs(REDLED);
            HAL_Timer_delay_ms(10);
            HW_switchLEDs(LEDOFF);

            sram_buf[NFC_MEM_SRAM_SIZE - 4] = 'N';
            sram_buf[NFC_MEM_SRAM_SIZE - 3] = 'A';
            sram_buf[NFC_MEM_SRAM_SIZE - 2] = 'K';
        } else {
            sram_buf[NFC_MEM_SRAM_SIZE - 4] = 'A';
            sram_buf[NFC_MEM_SRAM_SIZE - 3] = 'C';
            sram_buf[NFC_MEM_SRAM_SIZE - 2] = 'K';
        }

        NFC_SetTransferDir(ntag_handle, I2C_TO_RF);
        NFC_SetPthruOnOff(ntag_handle, TRUE);

        // write back Data
        NFC_WriteBytes(ntag_handle, NFC_MEM_ADDR_START_SRAM, sram_buf,
                NFC_MEM_SRAM_SIZE);

    其在I2C写入数据之前,设置了 NC_REG寄存器的 PTHRU_DIR 位。其是 NC_REG的第0位, 将这一步代码加入后,果然没问题了。

    完整测试代码:

     复制代码 隐藏代码
    #include<Wire.h>
    #include<Arduino.h>

    #define NT3H_I2C_ADDR 0x55
    #define SRAM_START_ADDR 0xf8
    #define SRAM_END_ADDR   0xFB

    #define NT3H1101_NC_REG      0x00 
    #define NT3H1101_SESSION_REG 0xFE

    #define SRAM_SIZE 64

    boolean flag = true;
    uint32_t haveWriten = 0;
    uint32_t allCount = 0;
    bool stopFlag = false;

    voidwriteRegister(uint8_t reg, uint8_t value){
      Wire.beginTransmission(NT3H_I2C_ADDR);
      Wire.write(0xfe);
      Wire.write(0x00);
      Wire.write(reg);   
      Wire.write(value); 
    uint8_t error = Wire.endTransmission();

    if (error == 0) {
        Serial.print("Write successful to register 0x");
        Serial.println(reg, HEX);
      } else {
        Serial.print("Error writing to register 0x");
        Serial.print(reg, HEX);
        Serial.print(": Error code ");
        Serial.println(error);
      }
    }

    voidsetI2CtoNFC(){
    stopPassthrough();
      Wire.beginTransmission(NT3H_I2C_ADDR);
      Wire.write(0xfe);
      Wire.write(0x00);
      Wire.write(0x1);
      Wire.write(0x0);   
          uint8_t error = Wire.endTransmission();

        if (error == 0) {
          Serial.println("setI2CtoNFC success");
        } else {
          Serial.println("setI2CtoNFC fail");
      }
    startPassthrough();
    }

    voidsetNFCtoI2C(){
    //passthrough and NFCtoI2c
    stopPassthrough();

      Wire.beginTransmission(NT3H_I2C_ADDR);
      Wire.write(0xfe);
      Wire.write(0x00);
      Wire.write(0x1);
      Wire.write(0x1);   
          uint8_t error = Wire.endTransmission();

        if (error == 0) {
          Serial.println("setI2CtoNFC success");
        } else {
          Serial.println("setI2CtoNFC fail");
      }
    startPassthrough();
    }

    voidstartPassthrough(){
      Wire.beginTransmission(NT3H_I2C_ADDR);
      Wire.write(0xfe);
      Wire.write(0x00);
      Wire.write(0x40);
      Wire.write(0x40);   
      Wire.endTransmission();
    }

    voidstopPassthrough(){
      Wire.beginTransmission(NT3H_I2C_ADDR);
      Wire.write(0xfe);
      Wire.write(0x00);
      Wire.write(0x40);
      Wire.write(0x0);   
      Wire.endTransmission();
    }

    boolean checkReady(){
        byte value;
        Wire.beginTransmission(NT3H_I2C_ADDR);
        Wire.write(0xFE);
        Wire.write(0x06);
        Wire.endTransmission();
        Wire.requestFrom(NT3H_I2C_ADDR,1);
        if (Wire.available() == 1) {
          value=Wire.read();
        }
        return value & 0b10000;
    }

    voidWriteACK(uint8_t *dataBuffer)
    {
        setI2CtoNFC();
        // uint8_t dataBuffer[16] = {0};
        dataBuffer[SRAM_SIZE-4] = 'A';
        dataBuffer[SRAM_SIZE-3] = 'C';
        dataBuffer[SRAM_SIZE-2] = 'K';

        Wire.beginTransmission(NT3H_I2C_ADDR);
        Wire.write(0xfb);
        for (size_t i = 0; i < 16; i++)
        {
            Wire.write(dataBuffer[i+3*16]);
        }
        uint8_t error = Wire.endTransmission();

        if (error == 0) {
          Serial.print("ACK successful to register 0x");
          Serial.println(0xfb,HEX);
        } else {
          Serial.print("ACK fail to register 0x");
          Serial.println(0xfb,HEX);
          Serial.println(error);
        }
        setNFCtoI2C();
    }

    voidReadDataBlock(const byte block_address, uint8_t *out_buffer, int out_buffer_length)
    {
          Wire.beginTransmission(NT3H_I2C_ADDR);
          Wire.write(block_address);
          Wire.endTransmission();
          Wire.requestFrom(NT3H_I2C_ADDR, out_buffer_length);
          if (Wire.available() == out_buffer_length) {
            for (int i = 0; i < out_buffer_length; i++)
            {
              out_buffer[i+16*(block_address-0xf8)] = Wire.read();
              haveWriten ++;
            }
          }
    }

    voidreadPages(uint8_t startPage, uint8_t endPage, uint8_t *data64){
        for (uint8_t page = startPage; page <= endPage; page++) {
              ReadDataBlock(page, data64,16);
              Serial.println(page);
        }
    }

    voidcheckFP(uint8_t *data){
        uint8_t pg_data[48] = {0};
       if (memcmp(data, pg_data, 48) == 0 && data[SRAM_SIZE-4] == 'F' && data[SRAM_SIZE-3] == 'P')
       {
          allCount = data[SRAM_SIZE-5] + data[SRAM_SIZE-6] << 8;
          haveWriten = 0;
          Serial.print("allCount: ");
          Serial.println(allCount);

       }elseif (memcmp(data, pg_data, 48) == 0 && data[SRAM_SIZE-4] == 'F' && data[SRAM_SIZE-3] == 'S')
       {
          stopFlag = true;
       }

    }

    voidsetup(){
      Wire.begin();    
      Serial.begin(115200);  
      Wire.setClock(300000);
    }

    voidprintHex(int data){
    if (data < 16) {
        Serial.print("0");
      }
      Serial.print(data, HEX);
      Serial.print(" ");
    }

    voidloop(){
          while (!stopFlag){
            startPassthrough();
            if (checkReady()){
              byte data64[64] = {0};
              readPages(0xf8,0xfb, data64);                
              for (size_t i = 0; i < 64; i++)
              {
                printHex(data64[i]);
              }
              Serial.println();
              if (haveWriten >= allCount && allCount != 0)
                {
                    WriteACK(data64);
                    haveWriten = 0;
                }
              checkFP(data64);
            }

          }
          Serial.println("==========================");
    }

    到此 NT3H1101 这颗芯片算是研究完毕了,已经可以实现数据通过RF-> I2C 接口的传递了,为后面传递数据打下基础了。

    5. STM32L011D4P6 芯片

    这颗是意法半导体的一颗低功耗芯片,供电方式完全是靠 NT3H1101 能量收集提供的,所有也只有 L系列的芯片才能满足项目需求。
    我之前也没做过意法半导体软件的开发,所以查询了一些资料,发现 STM32CubeMX 这个工具很好用,开发效率大幅度提升。
    这颗芯片在这个项目中的职责是 通过I2C 接口对 NT3H1101 寄存器进行设置,图片数据接收,以及通过spi 协议驱动墨水屏。
    那么我需要完成以下工作:

  • 将上面的代码移植到stm32平台
  • 移植墨水屏驱动
  • 整合上述俩点
  • 这里为了快速调试,我依然画了一个PCB用作调试:


    这里我加了3个储能电容100uf,用于存储  NT3H1101 的提供的能量, 也可以为驱动墨水屏提供更稳定的电源。用了一个跳线冒,防止反向给LDO供电导致芯片损坏。

    关于对NT3H1101寄存器操作代码移植 没什么可说的,在 STM32CubeMX配置好对应的gpio功能即可,时钟什么的默认即可。

    这里说一下 对墨水屏驱动的移植,以及整合过程中出现的问题。
    墨水屏驱动 用spi协议驱动, 基本上SPI协议驱动都有俩个关键函数,sendCommand  sendData 其区别在于是否拉高DC线,在拉高时,就是sendData, 拉低时就是sendCommand, 剩下的就是按照 已有的驱动给这俩个函数喂入不同的数据,所以墨水屏驱动移植只需要修改 不同平台的SPI通信这类底层函数的调用,上层喂入数据复制粘贴即可。

    6. 图像数据的分析与构建

    整合过程中,我需要理解墨水屏的原始数据是怎么表示的,这样才能构建图片数据:
    在黑白墨水屏中的数据只用黑白俩种表示,不存在中间值(灰度屏除外),一个字节有8位,每一位都可以表示黑或白。
    那么一块 104x212 分辨率的墨水屏,其图像数据可以用 104x212/8 = 2756 字节表示。

    对于黑白红 三色墨水屏,也是类似原理,不过数据量需要x2才可以,分别表示在 104x212 空间内 白-黑 白-红 像素

    这里我写了一个将图像转换 黑白红 3色的算法,最后保存成墨水屏驱动所需的图像格式:

     复制代码 隐藏代码
    import cv2
    import numpy as np

    pal_arr = [
        # 黑白调色板 (索引0)
        [(000), (255255255)],
        # 黑红调色板 (索引1) - 注意:这里只有黑色和红色
        [(000), (00255)],
        # 黑、红、白三色调色板 (索引2)
        [(000), (00255), (255255255)]
    ]

    # EPD 显示配置
    epd_arr = [
        # 格式: [宽度, 高度, 调色板索引]
        [8006002],  # 使用三色调色板
    ]

    defget_near(r, g, b, is_red_palette=True):
        """
        改进的颜色判断函数
        对于红黑调色板,只有当像素与红色相似时才返回红色索引
        """

        ifnot is_red_palette:
            # 黑白调色板:简单亮度判断
            gray = 0.299 * r + 0.587 * g + 0.114 * b
            return0if gray < 128else1

        # 对于红黑调色板,进行更复杂的颜色判断

        # 计算亮度
        gray = 0.299 * r + 0.587 * g + 0.114 * b

        # 计算与纯红色的相似度
        red_similarity = r - max(g, b)

        # 计算与纯黑色的相似度
        black_similarity = 255 - gray  # 值越大表示越接近黑色

        # 判断逻辑:
        # - 如果很暗,使用黑色
        # - 如果与红色相似且不是太暗或太亮,使用红色
        # - 否则根据亮度使用黑色或白色

        if gray < 60:  # 很暗的区域
            return0# 黑色
        elif gray > 200:  # 很亮的区域
            return2# 白色
        elif red_similarity > 30and80 < gray < 180:  # 与红色相似且中等亮度
            return1# 红色
        else:
            # 其他情况根据亮度决定
            return0if gray < 128else2

    defadd_val(err_arr, r, g, b, factor):
        """
        将误差值乘以系数后加到误差数组中
        """

        factor /= 16.0
        return [
            err_arr[0] + r * factor,
            err_arr[1] + g * factor,
            err_arr[2] + b * factor
        ]

    defproc_img(image,x=0, y=0, w=None, h=None):
        """
        处理图像的主函数

        参数:
        - image: 输入图像
        - is_red: 是否使用红黑调色板 (True) 或黑白调色板 (False)
        - x, y: 处理区域的起始坐标
        - w, h: 处理区域的宽度和高度
        """


        # 获取图像尺寸
        sH, sW = image.shape[:2]

        # 设置默认处理区域
        if w isNone:
            w = sW
        if h isNone:
            h = sH

        # 选择调色板
        epd_ind = 0# 使用第一个EPD配置
        pal_ind = epd_arr[epd_ind][2]

        cur_pal = pal_arr[pal_ind]

        # 创建输出图像
        output = np.zeros((h, w, 3), dtype=np.uint8)

        err_arr = [np.zeros((w, 3), dtype=np.float32) for _ inrange(2)]
        a_ind = 0
        b_ind = 1

        for j inrange(h):
            y_pos = y + j
            if y_pos < 0or y_pos >= sH:
                # 超出边界,使用棋盘格填充
                for i inrange(w):
                    color_idx = 0if (i + j) % 2 == 0else1
                    output[j, i] = cur_pal[color_idx]
                continue

            # 交换误差数组索引
            a_ind, b_ind = b_ind, a_ind
            # 重置当前行的误差
            err_arr[b_ind] = np.zeros((w, 3), dtype=np.float32)

            for i inrange(w):
                x_pos = x + i
                if x_pos < 0or x_pos >= sW:
                    # 超出边界,使用棋盘格填充
                    color_idx = 0if (i + j) % 2 == 0else1
                    output[j, i] = cur_pal[color_idx]
                    continue

                # 获取像素值和当前误差
                pixel = image[y_pos, x_pos]
                b, g, r = pixel  # OpenCV使用BGR格式
                old_err = err_arr[a_ind][i]

                # 添加误差到像素值
                r_new = r + old_err[0]
                g_new = g + old_err[1]
                b_new = b + old_err[2]

                # 钳制值到0-255范围
                r_new = np.clip(r_new, 0255)
                g_new = np.clip(g_new, 0255)
                b_new = np.clip(b_new, 0255)

                # 计算最接近的颜色索引
                color_idx = get_near(r_new, g_new, b_new)
                color_val = cur_pal[color_idx]

                # 设置输出像素
                output[j, i] = color_val

                # 计算误差
                r_err = r_new - color_val[2]  # 注意:OpenCV是BGR,红色在索引2
                g_err = g_new - color_val[1]
                b_err = b_new - color_val[0]

                # 扩散误差到相邻像素
                if i == 0:
                    # 第一列
                    if i < w - 1:
                        err_arr[b_ind][i] += np.array([r_err, g_err, b_err]) * (7.0 / 16.0)
                        err_arr[b_ind][i+1] += np.array([r_err, g_err, b_err]) * (2.0 / 16.0)
                        err_arr[a_ind][i+1] += np.array([r_err, g_err, b_err]) * (7.0 / 16.0)
                elif i == w - 1:
                    # 最后一列
                    err_arr[b_ind][i-1] += np.array([r_err, g_err, b_err]) * (7.0 / 16.0)
                    err_arr[b_ind][i] += np.array([r_err, g_err, b_err]) * (9.0 / 16.0)
                else:
                    # 中间列
                    err_arr[b_ind][i-1] += np.array([r_err, g_err, b_err]) * (3.0 / 16.0)
                    err_arr[b_ind][i] += np.array([r_err, g_err, b_err]) * (5.0 / 16.0)
                    err_arr[b_ind][i+1] += np.array([r_err, g_err, b_err]) * (1.0 / 16.0)
                    err_arr[a_ind][i+1] += np.array([r_err, g_err, b_err]) * (7.0 / 16.0)

        return output

    defdemo():
        width = 104
        heigh = 212

        width = 128
        heigh = 296

        # width = 128
        # heigh = 250
        test_image = cv2.imread("/home/wshuo/test.png")
        # test_image = cv2.imread("images2.jpg")
        # 显示原始图像
        height_,width_ = test_image.shape[:2]
        print(height_,width_)
        if width_ < height_:
            test_image = cv2.resize(test_image,(width,heigh))

        else:
            test_image = cv2.resize(test_image,(heigh,width))
            test_image = cv2.transpose(test_image)

        cv2.imshow("Original Image", test_image)

        # 误差扩散 + 红黑
        resultImg = proc_img(test_image)
        cv2.imshow("Error Diffusion (Red&Black)", resultImg)

        print("按任意键关闭窗口...")
        cv2.waitKey(0)
        cv2.destroyAllWindows()

        R = np.reshape(resultImg,(width*heigh,-1))
        red = [0]*(width*heigh//8)
        black = [0]*(width*heigh//8)
        # print(len(resultImg[2]))
        for index,i inenumerate(R):
            p_i = index // 8
            bit_i = 7 - (index % 8)
            # bit_i = index % 8
            mask_y = ~(1 << bit_i) & 0xff# 用作与 &
            mask_h = 1 << bit_i & 0xff# 用作或 |
            # print(bin(mask_y), bin(mask_h))
            ifsum(i) == 765:
                #white 红黑都置为1
                black[p_i] = black[p_i] | mask_h
                red[p_i] = red[p_i] | mask_h
            elifsum(i) == 255:
                #red 红置为0 黑置为1
                black[p_i] = black[p_i] | mask_h
                red[p_i] = red[p_i] & mask_y
            elifsum(i) == 0:
            # black 红置为1 黑置为0
                black[p_i] = black[p_i] & mask_y
                red[p_i] = red[p_i] | mask_h
            else:
                print("error")

        # R = black 
        R = black + red
        data = b"".join([i.to_bytes(1,byteorder='little'for i in R])
        withopen("img.data""wb"as f:
            f.write(data)

    demo()


    7. 整合图像接收与图像显示遇到的问题

    STM32L011  RAM只有 2KB, 小的可怜,根本无法存储一整张图像数据,所有这里不能采用预分配空间,接收完所有数据再去驱动屏幕显示,只能实时接收,实时写入到墨水屏的显存,数据接收完毕,调用显示函数,进行图像刷新。所以其中也遇到了一些问题。

  • 屏幕初始化时间过长,导致开启passthrogh模式延迟,app传输数据失败:
    这个也是我在原项目遇到的问题,解决方案尽快在主控mcu上电后,程序尽快进入到开启passthrough部分准备数据接收。对于无用的delay直接进行干掉(也不能都干掉,必要的reset dalay如果弄掉会导致显存没清除完全),其实问题根源还是在于app端,检测到芯片后立刻进行数据传输,而不是提供一个按钮,如果我写一个app的话这个问题可以完全解决。
  • 对于原项目的驱动电路有一些特例墨水屏无法驱动,测量后发现可能是boost升压达不到驱动电压要求,这个也是无源NFC供电导致的,在使用外部供电可以解决,我也尝试过修改升压电路中的电阻但是效果不是特别好,好在这类屏幕较少,大部分屏幕都可以完美驱动。
  • 原项目的 swclk, swdio, 俩个引脚被用作 驱动spi墨水屏,导致单片机在运行过程中无法进行调试和烧录,这个就很考验手速了,按下reset 后,立刻download新程序,在还没有对这俩个gpio初始化之前完成可以完成程序下载。当然也就无法进行任何调试了,因为调试也是依赖这俩个引脚,还好我测试板子接了个led灯,我通过控制 led灯亮灭判断程序执行到哪一步了 : )
  • 8. 完成第一版PCB绘制

    经过以上的研究,打样俩个测试板后,终于可以绘制这个pcb了。


    原理图:


    原理图对比原项目,去除了通过 GPIO 使能供电,增加了一个可以控制 led, 修改了天线。基本上硬件改动不大,主要还是摸索软件过程较为繁琐。

    9. 升级硬件

    原本我没打算升级,不过在购买芯片时发现NT3H2111 芯片比 NT3H1101 还便宜(搞不懂), NT3H2111 比NT3H1101多一个新功能fast write, 这个对RF->I2C传输速度会有质的提升,原来传输一张图像的速度可能需要10多秒,用了fast write后1、2秒完成。而且NT3H2111完全兼容 NT3H1101。

    然后STM32L011D4P6 也找到了更便宜但引脚更多的 STM32L011F4P6 (后来知道便宜是因为库存,现在更贵了)
    stm32替换后,多出的引脚可以用作调试了。下载程序也不用考验手速了 :)

    基于上述俩个芯片改动,重新绘制了PCB

    硬件的升级对软件部分核心逻辑基本不用动,只不过需要改动几个引脚映射。

    10. 升级硬件后遇到的问题

    没想到的是,当我用上fast write后(app端会自动判断芯片型号,选择用标准的write还是fast write),主控mcu处理对接收数据写入墨水屏显存速度达不到要求,在还没有写完的时候 RF->I2C的 下一份64字节数据就已经到了,导致APP等待读取时间过长从而传输失败。
    所以我stm32程序再次进行了修改。

    修改前:

     复制代码 隐藏代码
    EPD_SendData(data);

    voidEPD_SendData(UBYTE Data)
    {
        DEV_Digital_Write(EPD_DC_PIN, 1);
        DEV_Digital_Write(EPD_CS_PIN, 0);
        DEV_SPI_WriteByte(Data);
        DEV_Digital_Write(EPD_CS_PIN, 1);
    }

    修改后:

     复制代码 隐藏代码
    DEV_Digital_Write(EPD_DC_PIN, 1);
    DEV_Digital_Write(EPD_CS_PIN, 0);
    DEV_SPI_Write_nByte(data64, 64);
    DEV_Digital_Write(EPD_CS_PIN, 1);

    简单来说之前一次只传输一个像素点,但是每次都会重复拉高拉低 DC CS(这部分耗时还可以),主要还是单个SPI传输数据回比一次性传输64字节数据慢很多,修改之后就再没出现过传输失败了。

    但是也引入了个新问题:如果最后图片数据不是64的整数倍,会导致写入额外的垃圾数据,对于黑白墨水屏还好,因为额外的数据不会显示出来,但是对于黑白红的3色墨水屏,黑色传输完后,不是64整数倍,会导致额外的红色数据传输成黑色数据,最终效果是红色像素错位。
    所以我进行了这部分的计算,这里就不再贴了。后面我会把全部代码开源。

    11. 依然没有结束,子项目的开启

    我将做好的设备用滴胶封存,这样替代外壳,因为这玩意不用电。这也是最大优势,但是我将做好的成品发给我朋友时,他们手机NFC失败不到:) 可能是滴胶滴厚了,导致场识别不到(不同手机NFC感应距离也不太相同),当然也有天线设计问题。但是我不想在修改这个板子了,所以考虑替代方案。
    做一个板子 与 NT3H2111 进行数据交互,相当于是个图片烧录器,这样即使手机不支持NFC也能玩这个设备了,我选择pn532 小红板:


    这个设备感应场很强,即使间隔1-2cm也能识别到设备,但是网上很多资料都是对IC卡的解密,对ntag读写的资料比较少,这部分需要从头摸索。
    这个板子有几种模式:   

    模式作用HSU大部分解密IC卡都是通过此模式完成的,因为不需要主控MCU,直接用个USB-TTL小板即可与这个板子进行通信, 通过串口发送数据帧完成对目标卡的读写I2C需要主控MCU, 与这块板子进行I2C连接,发送数据帧实现对目标卡的读写SPI与I2C相同,只不过使用的协议是I2C

    3种模式可以通过板子上的拨码开关进行控制。

    主控使用 ESP-12F模组(这玩意买多了,得用上)

    目前这部分程序基本已经调通,底板还没到,等到了,经过我验证后把这部分也开源。

    12. 开源地址预留占位

    努力整理资料中。。。。。。。


    -官方论坛

    www.52pojie.cn


    收藏

    扫描二维码,在手机上阅读

    全网最小墨水屏:仅0.97寸

    冰箱贴一定要经常挪挪位置 墨水屏时钟冰箱贴也不例外

    评 论
    请登录后再评论