2024年1月

20240105,记。

最近在使用GPU对onnx模型进行加速过程中(仅针对N卡,毕竟也没有别的显卡了。。),遇到了点问题:就是明明在安装了合适版本的显卡驱动和CUDA后,onnx还是不能够成功调用GPU,并且还出现了先导入torch,再导入onnxruntime就可以成功调用的奇怪现象。

测试机器:Windows10,RTX 3070,onnxruntime-gpu==1.16.1,显卡驱动:522,CUDA11.8

问题展示:

onnxruntime.InferenceSession(ckpt, providers=['CUDAExecutionProvider'])
2024-01-05 10:44:22.7798928 [W:onnxruntime:Default, onnxruntime_pybind_state.cc:743 onnxruntime::python::CreateExecutionProviderInstance] Failed to create CUDAExecutionProvider. Please reference https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#requirements to ensure all dependencies are met.

在onnxruntime版本与CUDA等版本均对应,但却出现上面的警告信息,且没有查看到GPU调用。

下意识的会考虑是不是onnxruntime压根没找到GPU,所以尝试了下面的代码:

import onnxruntime
onnxruntime.get_device() # 得到的输出结果是GPU,所以按理说是找到了GPU的

解决之路:

自然是找issue是找官方仓库的issue是最靠谱的了,参考链接:
https://github.com/microsoft/onnxruntime/issues/11092

看完后真的给了我一种顿悟感,而且竟然还与torch莫名地产生了联系,一开始有大佬给出的解决方法是先
import torch

import onnxruntime
,但这个方法就很奇怪,我使用了onnx,自然是不想再引入torch这个包,而且torch还很大,没有解决根本问题。

之后有一个佬给出的回答才让我明白过来:

image-20240105105713683

我去查看了自己的torch的lib目录,发现下面确实有cudnn相关的动态链接库,但是去cuda的目录下(
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8
),却并没有发现相关的库,也就是说,平常我们安装好cuda就可以用torch了,是因为torch自带了cudnn,即使我们没有安装cudnn,也能够去使用(cuda属于是框架,cudnn是属于软件层面的加速库)。

但onnxruntime不一样,它并没有自带cudnn,所以是需要自己去加cudnn添加进cuda的对应目录下的,具体可参考
cudnn的安装教程
,还有官方教程写的也是比较清晰的:
Installation Guide - NVIDIA Docs

image-20240105666666908299

主要是解压下载的压缩包,复制cudnn的文件到cuda目录,以及添加环境变量,不过之前安装CUDA的时候应该已经默认添加过环境变量了。

完成这一步后,重新尝试了执行onnx推理,但却碰到了另一个问题:

Could not locate zlibwapi.dll. Please make sure it is in your library path!

重新查找后,根据
解决Could not locate zlibwapi.dll. Please make sure it is in your library path! - 知乎 (zhihu.com)
给出的回答,
NVIDIA官网已不再提供
zlibwapi.dll
的下载

,重新去
换了最新版本的cuDNN
,目前每个版本又分为与cuda对应的11.x和12.x的版本。版本对应可参考:
Support Matrix - NVIDIA Docs

image-20240105141214259

至此就解决了我碰到的onnxruntime无法在GPU推理的问题,关键点还是在于
按照以往torch使用的思路,以为安装了驱动和CUDA就可以了(或者说下意识以为cuDNN就已经一起安装好了,但其实并没有),但还需要添加cuDNN库,因为torch自带了,而onnxruntime没有自带

【Github:vue-viewer-editor/vve-i18n-cli】

这是我同事开发的国际化自动处理脚本,我进行过一次扩展,让其也支持我们一个 jQuery 老项目的国际化日常维护

至此,我们团队内的国际化均是使用该脚本来进行日常维护

该自动化脚本极大的为我们提效,基本将国际化的词条相关工作降低到 0 了,这意味着我们基本上不用特意留出太多时间来处理国际化方面的工作

但是,国际化其实不只有词条相关的工作,至于还有哪些工作,我之前发表过一篇
《项目国际化的难点痛点是什么》
里面吐槽得很清晰了

不可否认的是,如果没有这个自动化脚本,根本就没法在领导期望的时间内完成国际化的工作

自从有了这个脚本后,从以前的跟领导评估说要 4 天的国际化工作量到现在只评估了 1 天工作量,实际上跑下脚本分分钟就解决了,我还可以愉快的滑一天水,领导开心,我也开心~

自动化脚本能力

检查项目里是否存在不合理的编程方式,如中文做 key 值等

上述示例命令运行结果呈现:

{"type":"script-pre","text":"这里也可能有中文,还用不了this上下文"}
{"type":"props","text":"这里的中文也用不了this上下文"}
{"type":"zh-key","text":"中文"}

script-pre
场景是说发现有中文存在于 script 标签内,这部分代码运行在 js 模块作用域内,this 指向不是 vue 组件,包裹
this.$t
的话会导致程序异常,所以要先手动处理下,能下沉就下沉,否则就先手动用全局函数包裹,然后忽略这个处理

props
场景是说发现有中文存在于 vue 的 props 字段里,这里也无法访问 this,会报异常,建议这块更改成 computed 用法

zh-key
场景是说发现中文做 key 值,需要用户确认是否能被翻译处理

这个命令可以提前主动的发现代码里的国际化处理问题,避免将问题遗留到测试或线上阶段

标记无需处理的词条

总有些场景,你希望这个中文词条不要被国际化处理,这时候可以类似 es-lint 的忽略配置一样,既可以忽略整个文件,也可以忽略文件中的某个词条


vve-i18n-cli.config.js
里增加下忽略配置规则:

// vve-i18n-cli.config.js
module.exports = {
  // 国际化文本包裹相关
  zhWrap: {
    // 需要过滤的文件
    ignoreI18nFileRules: [],
    // 需要处理的文件
    i18nFileRules: ["!(node_modules|config|statsvnTmp)/**/*.+(js|vue)"],
    // 当词条前缀出现以下正则时,该词条过滤不处理
    ignorePreReg: [
      /t\s*\([\s\n]*$/, //  词条在 t( 方法内的不处理,$t() 符合该规则
      /console\.(?:log|error|warn|info|debug)\s*\(\s*$/, //  词条在 console.xxx 方法内的不处理,过滤掉日志内的中文处理
      new RegExp("//.+"), // 注释中的词条不处理
      new RegExp("i18n-disabled.+"), // 词条前面出现 i18n-disabled 关键词的不处理
    ],
  },
};

然后代码中这些场景就不被处理了:

const ZH_KEY = /*i18n-disabled*/ "出现忽略处理的关键词的词条也不会被处理";
// 注释里中文不会被处理
console.error("日志里的中文也不会被处理");

自动对各种场景的中文词条包裹翻译函数

当检查完代码基本没问题,也配置了需要忽略处理的词条文件后,就可以通过命令自动对中文词条进行包裹翻译函数处理了:

不管你项目文件有成百上千个,都是一个命令就自动完成国际化翻译函数包裹词条处理

包裹哪些文件、包含的翻译函数名,不同场景(js 里,vue 里)用什么函数包裹,js 里是否需要加入 import 引入包含函数的代码,哪些文件不处理,哪些词条不处理,哪些前缀标记的不处理等等

以上场景都是通过
vve-i18n-cli.config.js
配置文件处理,详情查看下面章节,有默认的配置,你也可以根据你项目需要进行自己诉求的配置

脚本不是写死的行为,通过不同配置,可以适应到各种项目里去使用,目前我们团队的老项目,新项目,各种项目就通过各自项目的配置来接入了这个国际化自动处理脚本

自动将所有词条提取到 json 文件中(按模块维护)

当项目完成的国际化包裹词条处理后,就可以接着下一步,把词条提取到 json 文件里了:

想机翻,可以,默认不翻译,只做提取
想按模块提取到不同 json 文件里,可以,配置下模块规则
想生成多份语言的 json,可以,默认只有 zh.json, en.json


国际化的词条工作无外乎就是词条包裹处理,词条提取,词条翻译

这些工作难度不大,但工作量大,借助这类国际化自动处理脚本,就可以极大的提高效率,开心的滑水了

如何使用

安装

npm install vve-i18n-cli -D

package 里添加脚本命令,简化命令使用

{
  "scripts": {
    "i18n": "vve-i18n-cli",
    "i18n-wrap": "vve-i18n-zh-wrap-cli",
    "i18n-check": "vve-i18n-zh-check-cli"
  }
}

根目录下创建配置文件 vve-i18n-cli.config.js

// vve-i18n-cli.config.js
module.exports = {
  // 工作目录
  cwd: ".",
  // 根目录,国际文本所在的根目录
  rootDir: "src",
  // 默认所有模块,如果有传module参数,就只处理某个模块
  // '**/module-**/**/index.js'
  moduleIndexRules: ["."],
  // 忽略模块
  ignoreModuleIndexRules: [],
  // 匹配含有国际化文本的文件规则
  i18nFileRules: ["**/*.+(vue|js)"],
  // 不匹配含有国际化文本的文件规则
  ignoreI18nFileRules: [],
  // 国际化文本的正则表达式,正则中第一个捕获对象当做国际化文本
  i18nTextRules: [/(?:[\$.])t\([\s\n]*['"](.+?)['"]/g],
  // 模块的国际化的json文件需要被保留下的key,即使这些组件在项目中没有被引用
  // 规则可以是一个字符串,正则,或者是函数
  keepKeyRules: [
    /^G\/+/, // G/开头的会被保留
  ],
  // 忽略国际化KEY的规则
  // 规则可以是一个字符串,正则,或者是函数
  ignoreKeyRules: [],
  // 生成的国际化资源包的输出目录
  outDir: "lang",
  // 生成的国际化的语言
  i18nLanguages: [
    "zh", // 中文
    "en", // 英文
  ],
  // 配置文件的路径,没有配置,默认路径是在${cwd}/vve-i18n-cli.config.js
  config: undefined,
  // 是否取配置文件
  disableConfigFile: false,
  // 是否翻译
  translate: false,
  // 翻译的基础语言,默认是用中文翻译
  translateFromLang: "zh",
  // 是否强制翻译,即已翻译修改的内容,也重新用翻译生成
  forceTranslate: false,
  // 翻译的语言
  translateLanguage: ["zh", "en"],
  // 非中文使用拼音来来翻译
  translateUsePinYin: false,
  // 模块下${outDir}/index.js文件不存在才拷贝index.js
  copyIndex: false,
  // 是否强制拷贝最新index.js
  forceCopyIndex: false,

  // 国际化文本包裹相关
  zhWrap: {
    cwd: ".",
    // 根目录,国际文本所在的根目录
    rootDir: ".",
    ignoreI18nFileRules: [],
    i18nFileRules: ["!(node_modules|config|statsvnTmp)/**/*.+(js|vue)"],
    ignorePreReg: [
      /t\s*\([\s\n]*$/,
      /tl\s*\([\s\n]*$/,
      /console\.(?:log|error|warn|info|debug)\s*\(\s*$/,
      /\/\/\s*$/,
      new RegExp("//.+"),
      new RegExp("i18n-disabled.+"),
    ],
    ignoreText: ["^[\\u4e00-\\u9fa5a-zA-Z0-9“._=,':;*#!”-]+$"],
    // js相关文件需要引入的国际化文件
    i18nImportForJs: "",
    // js相关文件需要使用国际化方法
    jsI18nFuncName: "$i18n.$t",
    // vue相关文件需要使用的国际化方法
    vueI18nFuncName: "$t",
  },
  zhCheck: {
    cwd: ".",
    // 根目录,国际文本所在的根目录
    rootDir: ".",
    ignoreI18nFileRules: [],
    i18nFileRules: ["!(node_modules|config|statsvnTmp)/**/*.+(vue|js)"],
    // 反引号中需要忽略的文本规则,可以是正则或者字符串
    ignoreTextInQuoteRules: [/t\(/],
  },
};

先检查是否存在不合理的代码实现

npm run i18n-check

再执行自动对词条包裹翻译函数的命令

npm run i18n-wrap

最后把这些被翻译函数包裹的词条提取到 json 文件里

npm run i18n


这份脚本很通用化,根据各自配置规则,可以适应到各种项目里面,实在不行,代码也是开源的,拉下来修修改改得了

扩展

我们团队的翻译不是机翻,而是有专门的翻译团队进行翻译,因此提取完 json 词条后,还需要用 excel 跟翻译团队打交道

所以可以来扩展下几个脚本

提取未翻译词条到 excel 文件中

/**
 * 抽取未翻译的词条到excel文件中
 */
const map = require("map-stream");
const path = require("path");
const vfs = require("vinyl-fs");
const XLSX = require("xlsx");

const ROOT_DIR = path.resolve("./");
const fileRules = [
  "**/*/i18n/en.json",
  // "**/eweb-setting-planningDeployment/i18n/en.json",
];

const writeExcel = (arr, name = "未翻译词条") => {
  const sheet_data = arr.map((v) => {
    return {
      中文: v,
      English: "",
    };
  });
  const new_sheet = XLSX.utils.json_to_sheet(sheet_data);
  // // 创新一个新的excel对象,就是workbook
  const new_workbook = XLSX.utils.book_new();
  // // 将表的内容写入workbook
  XLSX.utils.book_append_sheet(new_workbook, new_sheet, "sheet1");
  XLSX.writeFile(new_workbook, `${name}.xlsx`);
};

function run() {
  const zhList = [];
  console.log("================================>start", ROOT_DIR);
  vfs
    .src(
      fileRules.map((item) => path.resolve(ROOT_DIR, item)),
      {
        ignore: ["node_modules/**/*", "statsvnTmp/**/*"],
      }
    )
    .pipe(
      map((file, cb) => {
        console.log("处理文件 =========================>", file.path);

        let fileContent = file.contents.toString();
        fileContent = JSON.parse(fileContent);
        Object.keys(fileContent).map((zh) => {
          if (zh.match(/[\u4E00-\u9FFF]/)) {
            if (zh === fileContent[zh]) {
              // 未翻译
              zhList.push(zh);
            }
          }
        });
        cb();
      })
    )
    .on("end", () => {
      const uniZh = Array.from(new Set(zhList));
      writeExcel(uniZh);
      console.log("未翻译词条数量:", uniZh.length);
      console.log(
        "================================>end",
        "根目录下生成 excle 文件"
      );
    });
}

run();

将 excel 中的词条回填到 json 文件中

/**
 * 将翻译后的内容替换到en.json文件中
 */
const map = require("map-stream");
const path = require("path");
const vfs = require("vinyl-fs");
const fs = require("fs");
const XLSX = require("xlsx");

const ROOT_DIR = path.resolve("./");
const fileRules = ["**/*/i18n/en.json"];
// 文件名称 默认名称 resource.json
const fileName = "resource";
const fileJsonName = fileName + ".json";
const fileXlsName = fileName + ".xlsx";

const excelReader = (exlcePathArray = []) => {
  if (!Array.isArray(exlcePathArray)) {
    exlcePathArray = [exlcePathArray];
  }
  const obj = {};
  for (const i in exlcePathArray) {
    if (Object.hasOwnProperty.call(exlcePathArray, i)) {
      const excleFilePath = exlcePathArray[i];
      console.log("读取excle " + excleFilePath);
      const workbook = XLSX.readFileSync(excleFilePath, {
        type: "binary",
      });
      for (const sheet in workbook.Sheets) {
        const dataArray = XLSX.utils.sheet_to_json(workbook.Sheets[sheet]);
        obj[sheet] = dataArray;
      }
    }
  }
  return obj;
};

// 如果未找到 resource.json 查找 excel文件
if (!fs.existsSync(path.resolve(ROOT_DIR, fileJsonName))) {
  // 判断resource.excel是否存在
  if (fs.existsSync(path.resolve(ROOT_DIR, fileXlsName))) {
    let fileObj = excelReader(path.resolve(ROOT_DIR, fileXlsName));
    let dataObj = {};
    if (fileObj.sheet1) {
      // 中文列存在 例子: {"中文":"下载中...","English":"down…"}
      for (let i = 0; i < fileObj.sheet1.length; i++) {
        let itemZhKey = fileObj.sheet1[i]["中文"];
        let itemEnKey = fileObj.sheet1[i]["English"];
        if (itemZhKey && itemEnKey) {
          dataObj[itemZhKey] = itemEnKey;
        }
      }
    }
    // 获取sheet1的内容
    const data = JSON.stringify(dataObj);
    try {
      fs.writeFileSync(path.resolve(ROOT_DIR, fileJsonName), data);
    } catch (error) {
      console.log("生成文件 resource.xlsx 异常");
      throw error;
    }
  } else {
    console.log("不存在文件 resource.xlsx");
    throw new Error("不存在文件 resource.xlsx");
  }
}

const originResource = require(path.resolve(ROOT_DIR, fileJsonName));
let resource = Object.assign({}, originResource);
Object.keys(originResource).map((key) => {
  resource[`${key}:`] = `${originResource[key]}:`;
  resource[`${key}:`] = `${originResource[key]}:`;
  resource[`${key})`] = `${originResource[key]})`;
  resource[`${key})`] = `${originResource[key]})`;
  resource[`${key} `] = `${originResource[key]} `;
  resource[` ${key}`] = ` ${originResource[key]}`;
  resource[`(${key}`] = `(${originResource[key]}`;
  resource[`(${key}`] = `(${originResource[key]}`;
  resource[` ${key} `] = ` ${originResource[key]} `;
});

function run() {
  console.log("================================>start", ROOT_DIR);
  let failedCount = 0;
  let successCount = 0;
  let failedZhs = [];
  vfs
    .src(
      fileRules.map((item) => path.resolve(ROOT_DIR, item)),
      {
        ignore: ["node_modules/**/*", "statsvnTmp/**/*"],
      }
    )
    .pipe(
      map((file, cb) => {
        console.log("处理文件 =========================>", file.path);

        let fileContent = file.contents.toString();
        fileContent = JSON.parse(fileContent);
        let hasChange = false;
        Object.keys(fileContent).map((zh) => {
          if (zh.match(/[\u4E00-\u9FFF]/)) {
            if (zh === fileContent[zh]) {
              // 未翻译
              if (resource[zh] && resource[zh] !== zh) {
                hasChange = true;
                fileContent[zh] = resource[zh];
                successCount++;
              } else {
                failedCount++;
                failedZhs.push(zh);
              }
            }
          }
        });
        if (hasChange) {
          fs.writeFileSync(
            file.path,
            JSON.stringify(fileContent, " ", 2) + "\n"
          );
        }
        cb();
      })
    )
    .on("end", () => {
      fs.writeFileSync(
        "unHandle.json",
        JSON.stringify(failedZhs, " ", 2) + "\n"
      );
      console.log("本次翻译成功词条数量:", successCount);
      console.log("还剩余未翻译词条数量:", failedCount);
      console.log("================================>end");
    });
}

run();

Pulsar3.0-NewFeature.png

在上一篇文章
Pulsar3.0 升级指北
讲了关于升级 Pulsar 集群的关键步骤与灾难恢复,本次主要分享一些
Pulsar3.0
的新功能与可能带来的一些问题。

升级后所遇到的问题

先来个欲扬先抑,聊聊升级后所碰到的问题吧。

其中有两个问题我们感知比较明显,特别是第一个。

topic被删除

我们在上个月某天凌晨从
2.11.2
升级到
3.0.1
之后,进行了上一篇文章中所提到的功能性测试,发现没什么问题,觉得一切都还挺顺利的,半个小时搞定后就下班了。

结果哪知道第二天是被电话叫醒的,有部分业务反馈业务重启之后就无法连接到 Pulsar 了。

image.png
最终定位是 topic 被删除了。

其中的细节还蛮多的,修复过程也是一波三折,后面我会单独写一篇文章来详细梳理这个过程。

在这个 issue 和 PR 中有详细的描述:
https://github.com/apache/pulsar/issues/21653
https://github.com/apache/pulsar/pull/21704

感兴趣的朋友也可以先看看。

监控指标丢失

第二个问题不是那么严重,是升级后发现 bookkeeper 的一些监控指标丢失了,比如这里的写入延迟:
image.png
我也定位了蛮久,但不管是官方的 docker 镜像还是源码编译都无法复现这个问题。

最终丢失的指标有这些:

  • bookkeeper_server_ADD_ENTRY_REQUEST
  • bookkeeper_server_ADD_ENTRY_BLOCKED
  • bookkeeper_server_READ_ENTRY_BLOCKED
  • bookie_journal_JOURNAL_CB_QUEUE_SIZE
  • bookie_read_cache_hits_count
  • bookie_read_cache_misses_count
  • bookie_DELETED_LEDGER_COUNT
  • bookie_MAJOR_COMPACTION_COUNT

详细内容可以参考这个 issue:
https://github.com/apache/pulsar/issues/21766

新特性

讲完了遇到的 bug,再来看看带来的新特性,重点介绍我们用得上的特性。

支持低负载均衡

image.png

当我们升级或者是重启 broker 的时候,全部重启成功后其实会发现最后重启的那个 broker 是没有流量的。

这个原理和优化在之前写过的
Pulsar负载均衡原理及优化
其实有详细介绍。

本次 3.0 终于将那个优化发版了,之后只要我们配置
lowerBoundarySheddingEnabled: true
就能开启这个低负载均衡的一个特性,使得低负载的 broker 依然有流量进入。

跳过空洞消息

image.png
Pulsar 可能会因为消息消费异常导致游标出现空洞,从而导致磁盘得不到释放;

所以我们有一个定时任务,会定期扫描积压消息的 topic 判断是否存在空洞消息,如果存在便可以在管理台使用 skipMessage API 跳过空洞消息,从而释放磁盘。

但在 3.0 之前这个跳过 API 存在 bug,只要跳过的数量超过 8 时,实际跳过的数量就会小于 8.

具体 issue 和修复过程在这里:
https://github.com/apache/pulsar/issues/20262
https://github.com/apache/pulsar/pull/20326

总之这个问题在 3.0 之后也是修复了,有类似需求的朋友也可以使用。

新的负载均衡器

同时也支持了一个新的负载均衡器,解决了以下问题:

  • 以前的负载均衡大量依赖 zk,当 topic 数量增多时对扩展性带来问题。
    • 新的负载均衡器使用
      non-persistent
      来存储负载信息,就不再依赖 zk 。
  • 以前的负载均衡器需要依赖
    leader broker
    进行重定向到具体的 broker,其实这些重定向并无意义,徒增了系统开销。
    • 新的负载均衡器使用了 SystemTopic 来存放 topic 的所有权信息,这样每个 broker 都可以拿到数据,从而不再需要从 leader broker 重定向了。

更多完整信息可以参考这个 PIP:
PIP-192: New Pulsar Broker Load Balancer

支持大规模延迟消息

第二个重大特性是支持大规模延迟消息,相信是有不少企业选择 Pulsar 也是因为他原生就支持延迟消息。

我们也是大量在业务中使用延迟消息,以往的延迟消息有着以下一些问题:

  • 内存开销过大,延迟消息的索引都是保存在内存中,即便是可以分布在多个 broker 中分散存储,但消耗依然较大
    • 重点优化了索引的内存占有量。
  • 重启 broker 时会消耗大量时候重建索引
    • 支持了索引快照,最大限度的降低了构建索引的资源消耗。

待优化功能

监控面板优化

最后即便是升级到了 3.0 依然还有一些待优化的功能,在之前的
从 Pulsar Client 的原理到它的监控面板
中有提到给客户端加了一些监控埋点信息。

最终使用下来发现还缺一个 ack 耗时的一个面板,其实日常碰到最多的问题就是突然不能消费了(或者消费过慢)。

这时如果有这样的耗时面板,首先就可以定位出是否是消费者本身的问题。

image.png
目前还在开发中,大概类似于这样的数据。

总结

Pulsar3.0 是 Pulsar 的第一个 LTS 版本,推荐尽快升级可以获得长期支持。
但只要是软件就会有 bug,即便是 LTS 版本,所以大家日常使用碰到 Bug 建议多向社区反馈,一起推动 Pulsar 的进步。


大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是
在i.MXRT1170上快速点亮一款全新LCD屏的方法与步骤

我们知道 LCD 屏的接口有很多:DPI-RGB、MIPI DSI、DBI/MCU(I8080)、LVDS、SPI 等等,接口不同,对应的软件驱动也不同。RT1170 片内外设对以上接口都能很好地支持,今天我们主要聊最近比较火的 MIPI DSI 接口。

在恩智浦官方 SDK (v2.14) 里目前支持的 MIPI DSI 接口的 LCD 屏主要有如下四款,但客户在实际应用中选择的屏五花八门(生产 MIPI DSI 接口的 LCD 厂商非常多),如果我们拿到一款全新 LCD 屏,该如何快速点亮它呢?今天痞子衡教你方法:

LCD屏型号 LCD分辨率 LCD驱动IC
晶鸿电子 RK055AHD091 720x1280 瑞鼎科技 RM68200
晶鸿电子 RK055MHD091 720x1280 奇景光电 HX8394-F
晶鸿电子 RK055IQH091 540x960 瑞鼎科技 RM68191
定制屏 G1120B0MIPI 390x390 瑞鼎科技 RM67162

一、点屏准备工作

磨刀不误砍柴工,在开始点屏之前我们需要准备如下材料,这在后续修改和调试 LCD 屏相关代码时非常重要。其中 LCD 屏数据手册一般需要向屏厂获取,有了屏数据手册我们就能知道其相应驱动 IC,从而下载这个驱动 IC 的数据手册。

1. LCD 屏配套的数据手册
2. LCD 屏内置驱动 IC 的数据手册
3. RT1170 板卡连接 LCD 屏的原理图
4. 恩智浦 SDK_2_14_0_MIMXRT1170-EVKB
5. 能够访问 github

痞子衡就以深圳柯达科电子生产的 KD050FWFIA019-C019A 屏为例,这款 MIPI DSI 屏分辨率是 480x854,其驱动 IC 是来自奕力科技的 ILI9806E。

二、点屏标准步骤

2.1 熟悉SDK标准例程

恩智浦 SDK 里的 elcdif_rgb 例程是一个很好的基础工程,我们可以基于这个工程来修改代码做调试。工程里我们主要关注 elcdif_support.c/h 文件,在这个文件里,恩智浦已经把不同屏之间的差异做了抽离处理,你搜索 MIPI_PANEL_ 宏就能找到那些差异,这些差异的地方就是我们需要改动的地方。

\SDK_2_14_0_MIMXRT1170-EVKB\boards\evkbmimxrt1170\driver_examples\elcdif\rgb\cm7\iar

这个 elcdif_rgb 例程里没有看到 G1120B0MIPI 身影,因为小分辨率的圆屏不太适合这个 example,其驱动可在 RT595_SDK\boards\evkmimxrt595\vglite_examples 里找到。

2.2 调整屏控制I/O脚(Power_en、Reset、Backlight)

先来关注硬件上需要注意的改动,RT1170 上 MIPI DSI 这个外设不同于其它外设有很多 pinmux 选项,其就一组固定的引脚(并且是专用的),所以这组引脚我们不需要做任何代码上的配置。

但是 LCD 屏除了 MIPI DSI 相关信号以及电源、地之外,通常还有三个控制信号,即 Power_en(电源使能-可选)、Reset(硬复位)、Backlight(背光控制),这三个信号一般是通过普通 GPIO 来控制的。

所以我们需要打开板卡原理图,找到 LCD 相关连接把这三个信号所用的 GPIO 找出来,并在代码里如下地方做相应改动:

elcdif_rgb 例程会在共享函数 BOARD_InitLcdPanel() 里操作 BOARD_MIPI_PANEL_BL 宏所指向的 GPIO 来打开背光。此外 BOARD_MIPI_PANEL_RST 和 BOARD_MIPI_PANEL_POWER 宏所指向的 GPIO 操作已经被封装在如下函数里,这个函数被进一步封装进 display_handle_t 里供后续驱动灵活使用:

static void PANEL_PullResetPin(bool pullUp);
static void PANEL_PullPowerPin(bool pullUp);

2.3 创建LCD驱动IC源文件

现在我们需要在如下目录下,创建 ILI9806E 的驱动文件,可以先直接拷贝 hx8394 文件夹下的文件并将其改名后添加进工程,并且在 elcdif_support.c/h 里也复制添加相应代码保证编译通过(后续再参考 ILI9806E 数据手册修改代码)。

\SDK_2_14_0_MIMXRT1170-EVKB\components\video\display

2.4 调整屏上电复位延时(Power_en、Reset)

有了 fsl_ili9806e.c/h 基本源文件后,现在我们需要根据 ILI9806E 数据手册来修改代码。首先是调整屏上电以及复位延时时间,这个延时一般既可以在 KD050FWFIA019-C019A 屏的数据手册也可以在 ILI9806E 的数据手册里找到。

有了延时数值之后,在 ILI9806E_Init() 函数里做相应设置即可:

status_t ILI9806E_Init(display_handle_t *handle, const display_config_t *config)
{
    const ili9806e_resource_t *resource = (const ili9806e_resource_t *)(handle->resource);
    /* Only support 480 * 854 */
    if (config->resolution != FSL_VIDEO_RESOLUTION(480, 854))
    {
        return kStatus_InvalidArgument;
    }
    /* Power on. */
    resource->pullPowerPin(true);
    ILI9806E_DelayMs(1U);
    /* 根据屏数据手册调整复位延时时间. */
    resource->pullResetPin(true);
    ILI9806E_DelayMs(10U);
    resource->pullResetPin(false);
    ILI9806E_DelayMs(10U);
    resource->pullResetPin(true);
    ILI9806E_DelayMs(120U);
    
    /* 代码省略 */
}

2.5 调整屏显示相关参数

现在我们需要在 elcdif_support.h 里根据 KD050FWFIA019-C019A 屏的数据手册修改如下定义包含:屏分辨率、六个行列扫描参数、四个信号极性(APP_POL_FLAGS)、数据位宽,这些都是屏本身的特性。

#if (USE_MIPI_PANEL == MIPI_PANEL_KD050FWFIA019)
#define APP_PANEL_HEIGHT 854
#define APP_PANEL_WIDTH  480
#define APP_HSW          4
#define APP_HFP          18
#define APP_HBP          30
#define APP_VSW          4
#define APP_VFP          20
#define APP_VBP          30
#endif
#define APP_POL_FLAGS \
    (kELCDIF_DataEnableActiveHigh | kELCDIF_VsyncActiveLow | kELCDIF_HsyncActiveLow | kELCDIF_DriveDataOnFallingClkEdge)

#define APP_DATA_BUS       24
#define APP_LCDIF_DATA_BUS kELCDIF_DataBus24Bit

关于六个行列扫描参数(HSW/HFP/HBP/VSW/VFP/VBP)稍稍科普一下,这些信号是以行列同步信号(VSYNC/HSYNC)为时间起点来做的延时,相当于在实际显示的图像宽高基础上做了外围扩大,从而提高图像有效区域显示的可靠性(实际上是等待面板做好每行数据刷新前的准备工作)。

分辨率和行列扫描参数均设置正确了之后,别忘了根据想要的刷新率(比如 60Hz)计算得出所需的 pixel clock,在 BOARD_InitLcdifClock() 函数里做相应设置。

void BOARD_InitLcdifClock(void)
{
    /*
     * The pixel clock is (height + VSW + VFP + VBP) * (width + HSW + HFP + HBP) * frame rate.
     * Use PLL_528 as clock source.
     * For 60Hz frame rate, the KD050FWFIA019 pixel clock should be 29MHz.
     */
    const clock_root_config_t lcdifClockConfig = {
        .clockOff = false,
        .mux      = 4, /*!< PLL_528. */
#if (USE_MIPI_PANEL == MIPI_PANEL_RK055AHD091) || (USE_MIPI_PANEL == MIPI_PANEL_RK055MHD091)
        .div = 9,
#elif (USE_MIPI_PANEL == MIPI_PANEL_RK055IQH091)
        .div = 15,
#elif (USE_MIPI_PANEL == MIPI_PANEL_KD050FWFIA019)
        // 我们需要设置 29MHz 的 pixel clock
        .div = 18,
#endif
    };
    CLOCK_SetRootClock(kCLOCK_Root_Lcdif, &lcdifClockConfig);
    mipiDsiDpiClkFreq_Hz = CLOCK_GetRootClockFreq(kCLOCK_Root_Lcdif);
}

2.6 配置LCD驱动芯片

现在到了最难也是最重要的环节了,KD050FWFIA019-C019A 面板主要是由 ILI9806E 芯片驱动的,ILI9806E 本身是个万能驱动芯片,其支持的接口很多,MIPI DSI 仅是其一,而且 2.5 节里设置的那些关于屏显示相关参数,我们都需要设置进 ILI9806E 内部寄存器里。

打开 ILI9806E 数据手册(V097版),一共 328 页,寄存器一大堆,我们难道要看着数据手册一个个去设置吗?当然不是!这时候需要打开万能的 github,搜索跟 ili9806e 相关的代码,看看前人有没有调试好的现成代码。

其实关于屏的支持,Linux 里做得比较多,痞子衡找了个 RaspberryPI 移植的分支,里面有 ili9806e 参数初始化表,注意这个表不一定完全适用 KD050FWFIA019-C019A(因为用 ILI9806E 芯片驱动的面板非常多),我们需要在这个参数表基础之上做一些调整。

https://github.com/raspberrypi/linux/blob/rpi-6.1.y/drivers/gpu/drm/panel/panel-ilitek-ili9806e.c

把 RaspberryPI 仓库里的参数表移植进我们的 fsl_ili9806e.c 文件里后,粗粗看了一下注释,其配置的是 480x800 的屏,极性设置相关也都和 KD050FWFIA019-C019A 有差异。

最后我们再对照 ILI9806E 数据手册里的寄存器定义做一些参数上的微调,如下四个寄存器需要重点关注。这些微调做完之后,把代码下载进板卡运行,这时候你应该能看到屏开始正常工作了。

至此,在i.MXRT1170上快速点亮一款全新LCD屏的方法与步骤痞子衡便介绍完毕了,掌声在哪里~~~

欢迎订阅

文章会同时发布到我的
博客园主页

CSDN主页

知乎主页

微信公众号
平台上。

微信搜索"
痞子衡嵌入式
"或者扫描下面二维码,就可以在手机上第一时间看了哦。