Frossky 发布的文章

文档

amazon官方文档, 中文好评!https://docs.amazonaws.cn/freertos/index.html
官网: https://freertos.org/
官方文档,英文的
AWS上的文档,也是英文的

FreeRTOS与RT-Thread对比: 后者自带很多第三方协议, 如i2c/mqtt等等, 而FreeRTOS似乎要自己找自己组装进去.

根据其他乱七八糟文档的整理

根据深入浅出,FreeRTOS新手+入门学习笔记整理.

  • 任务:
    xTaskCreate建立多个任务, 然后使用vTaskStartScheduler调度任务函数. 任务函数执行完成任务后应该进入等待.

  • 队列:
    xQueueCreate建立队列, xQueueReceive接收队列.

根据FreeRTOS操作系统最全面使用指南 整理
似乎就主要3个功能: 任务调度/FIFO队列通讯/内存管理.
时间管理主要就是一个延迟函数vTaskDelay

下面看官网教程https://www.freertos.org/FreeRTOS-quick-start-guide.htmlDeveloper Docs部分. 描述Kernel部分.

编码标准

FreeRTOS用的是MISRA标准.

前缀

变量类型 前缀
uint32_t ul
uint16_t us
uint8_t uc
有符号非标准int(如Type_t/TickType_t/size_t) x
无符号非标准int(如UbaseType_t) ux
enum类型 e
* 指针 p
函数类型 前缀
文件内函数(类似private) prv
void空返回函数 v
其他返回值函数 参照变量前缀

宏也有前缀, 应含有或部分含有所在的文件名(小写).其他部分为大写, 如: configUSE_PREEMPTION, 习惯了全大写的看起来怪怪的

移植层定义的适配类型

  • TickType_t:如果configUSE_16_BIT_TICKS为非零(条件为真),TickType_t定义为无符号16位类型。如果configUSE_16_BIT_TICKS为零(条件为假),TickType_t定义为无符号32位类型。注:32位架构的微处理器应设置configUSE_16_BIT_TICKS为零。
  • BaseType_t:定义为微处理器架构效率最高的数据类型。比如,在32位架构处理器上,BaseType_t应该定义为32位类型。在16位架构处理器上,BaseType_t应该定义为16位类型。如果BaseType_t定义为char,对于函数返回值一定要确保使用的是signed char,否则可能造成负数错误。
  • UbaseType_t:这是一个无符号BaseType_t类型

其他风格

  • 坚决不用 //注释, 哈哈. 一行80列以内.
  • 4格缩进
  • #include的顺序是标准库/自己的库/硬件特定的库
  • #include后面依次是#define, 文件内函数声明(都加了static), 文件内全局变量声明, 函数定义
  • 所有函数定义都有/*-------*/分行

任务task

FreeRTOS要做的跟Windows一样, 一个任务是一个窗口, 有自己的上下文和环境, 看上去就像独立运行的一样. 多个任务看起来就像是同时在运行, 实际上由任务调度去决定任务的切换, 每一时间只有一个任务占领CPU(当然Windows也是一样的)

任务状态

占领CPU的时候就是 running态, 暂时调度给其他任务就是ready态.
任务自己进入等待而把CPU主动让出来交Blocked态, 如调用了vBlockDelay()或等待其他事件. 等到了就恢复ready,或者实在等不到(超时timeout了)也恢复ready
啥也不干的任务可以调用vTaskSuspend()进入Suspended态, 跟Blocked区别就是不会超时. 只有调用xTaskResume()才会回到ready态.

任务优先级

数字高的先运行. 0是最低数字. 当前运行的就是最高优先级的一个或多个ready任务. 多个任务同时的话就用时间片切换.

优先级原则

  • Fixed priority固定优先级. 调度器不会更改优先级
  • Preemptive抢占式. 最高优先级永远先运行. 如果一个任务优先级被中断由低调到最高, 它立刻把抢到CPU, 无论其他任务运行到哪儿了, 即便是一个时间片没有执行完.
  • Round-robin同优先级循环切换
  • Time sliced时间分片.一个tick中断一片时间.

要小心饿死低优先级任务

高优先级永远最先运行, 如果占着CPU不放, 低优先级永远不会运行. 所以要高风亮节, 等事的时候就把CPU让出来, 进入Blocked或者Suspended, 事儿来了再处理.

优先级其他

抢占式和时间分片这两个特性可以在FreeRTOSConfig.h中关闭.
另外支持大小核(AMP)和同等多核(SMP)等CPU

任务实现

    void vATaskFunction( void *pvParameters )
    {
        for( ;; )
        {
            -- Task application code here. --
        }

        /* Tasks must not attempt to return from their implementing
        function or otherwise exit.  In newer FreeRTOS port
        attempting to do so will result in an configASSERT() being
        called if it is defined.  If it is necessary for a task to
        exit then have the task call vTaskDelete( NULL ) to ensure
        its exit is clean. 
        翻译: 任务别想着return, 新的port里这样做会导致configASSERT()
        调用报错. 如果非得退出, 调用vTaskDelete( NULL ) */
        vTaskDelete( NULL );
    }

函数形态咯. for(;;)的意思是任务一直执行, 直到vTaskDelete, 相当于windows的×. 任务里面是坚决不能有return的, 否则就会报错.
一直执行的任务里面, 自己决定是不是要Blocked或者Suspended把CPU让出来. 所以这个for(;;)在裸机系统里面看起来像是个死锁, 实际上却是让任务保持运行的方式, 毕竟它不能return
任务函数的类型是TaskFunction_t

    void vATaskFunction( void *pvParameters )
    {
        for( ;; )
        {
            /* Psudeo code showing a task waiting for an event 
            with a block time. If the event occurs, process it.  
            If the timeout expires before the event occurs, then 
            the system may be in an error state, so handle the
            error.  Here the pseudo code "WaitForEvent()" could 
            replaced with xQueueReceive(), ulTaskNotifyTake(), 
            xEventGroupWaitBits(), or any of the other FreeRTOS 
            communication and synchronisation primitives. 
            翻译: 下面伪码展示了任务等待事件的方式. 事件发生了就处理事件,
            超时了就处理报错. 伪码WaitForEvent可以换成 xQueueReceive(),
            ulTaskNotifyTake(), xEventGroupWaitBits()或其他通讯同步机制 */
            if( WaitForEvent( EventObject, TimeOut ) == pdPASS )
            {
                -- Handle event here. --
            }
            else
            {
                -- Clear errors, or take actions here. --
            }
        }

        /* As per the first code listing above. */
        vTaskDelete( NULL );
    }

任务使用 xTaskCreate() xTaskCreateStatic()创建, 使用vTaskDelete()销毁.

Co-routines协程

超小RAM的MCU可以用协程, 32位的CPU一般就用不到了. 这说的是Arduino的那个8位机吗? 这儿也不介绍了, 一般这种情况估计都用裸机了.

任务/中断间通讯机制

Queues队列

  • 队列是任务间以及中断和任务间的主要通讯方式. 采用的是先进先出FIFO, 跟排队一样, 先到先得.
  • 发到队列的数据是复制的, 这和其他消息机制()类似, 也就是发出去以后, 不管对方收了没有, 自己这一份数据都可以改了, 类似于函数调用的实参传递.
  • 中断使用队列和任务使用队列的API不一样, 方便识别.
  • 读取空队列的任务将进入Blocking态, 直到收到消息. 发送消息给满队列的任务将进入Blocking态, 直到队列有空.
  • 中断不允许调用以fromISR结尾的API函数. ISR意思是Interrupt Service Routine中断服务事务,就是中断处理函数.

Binary Semaphores二进制信号量

先说啥是信号量. 简单地说就是解决资源占用的方式, 跟十字路口的信号灯一样. 拿到绿灯的占用路口走, 给另一个方向的车辆红灯, 另一个方向的就等着(Blocking).有限的资源多个任务都想用, 就用信号量来解决. 也有点像停车场剩余车位数(计数信号量). 没车位的都在外面等着(Blocking).
这儿这儿有中文讲解.
互斥和同步. 互斥就是一个球只有一个人拍, "排斥"了其他人拍球, 想玩的排队. 同步就是想做才得先小工洗菜,再大厨炒菜,再跑堂端菜, 都是菜, 但就是得按顺序处理, 没到操作步骤的也排着队等.这儿看到同步也是一种互斥, 除了等资源, 还得步骤到了.
我在处理腾讯连连的MQTT时可能就是一种同步机制, 需要按顺序处理ready->GOT IP->时间->MQTT.只不过我是用enum的不同状态表示的.

二进制信号量就是占用时加锁, 释放时解锁, 表明这两种状态的.

Task Notifications任务通知有时候可以替代二进制信号量, 并且更轻量一些.

  • 二进制信号量同时用于同步和互斥机制.
  • 二进制信号量和mutexes互斥量很相似, 其差别只在于互斥有优先级继承, 而二进制信号量没有. 因此互斥量更适合互斥场景, 而二进制信号量更适合任务间(包括中断)同步场景.
  • 二进制信号量相当于只有1位的队列Queue.
  • 中断处理后才能执行的任务可以等待这个信号量, 而中断给出这个信号量,函数是xSemaphoreGiveFromISR()
  • 任务的优先级可以保证获取信号量以后的处理顺序(同步机制?).

Counting Semaphores计数信号量

同样 Task Notifications任务通知有时候做轻量级替代.

我在MQTT处理对pub消息的回应时用了类似的计数机制. 为防止在连续发出的第二条pub指令之后, 实际执行pub之前, 收到了上一条pub指令的success消息, 将pub指令取消导致第二条pub没有执行(同时还要兼顾失败重发机制), 我在pub发出指令后增加计数/在success时减少计数, 只有计数为0时才完全停止pub.

  • 相当于长度大于1的队列
  • 可用于事件的计数. 计数值就是未处理事件的值
  • 可用于资源管理. 跟停车场一个道理, 计数值表示空余车位的数量. 车走了加1车进来减1, 为0就等着.

Mutexes互斥量

用于对有限资源的访问.
互斥量借用完要自己还回去(只有1个且不会被消耗), 而二进制信号量用完不用还(持续被生产出来并且被消耗掉).
低优先级的任务持有互斥量时, 高优先级的再去获取, 会把低优先级的任务的优先级暂时提高到高优先级.
适合任务间确认谁访问资源, 不适合中断使用. 因为中断是不可能去等待任务给他返还可用性的.

Recursive Mutexes递归互斥量

可以被一个任务连续借用多次, 但也要被还回去这么多次, 其他任务才能用.

Task Notifications任务通知

算是新特性, 从v8.2.0开始引入, 在v10.4.0中增加了单任务多通知功能

  • 每个任务有1个或1组(v10.4以后)通知. 任务A可以发直达信息(direct, 一个32位的值)到任务B的通知里, 发到以后任务B的通知会变成Pending.
  • 任务B可以等待通知的Pending时Blocking.
  • 任务A发了direct后还能更新direct的值. 可以不管任务B读了没有, 也可以等待任务B读完再更新, 可以改1位或多位, 也可以递增
  • 任务B调用xTaskNotifyWait()/xTaskNotifyWaitIndexed()读了值以后, 通知状态就会变成Not Pending.
  • 任务B也可以直接调用xTaskNotifyStateClear()/xTaskNotifyStateClearIndexed()清除通知的Pending态
  • 即便有v10.4以后有一组通知, 任务B同时也只能等一个通知的Pending, 而不能同时等多个通知的Pending
  • 通知比之前那些方式要快45%, 并且使用RAM更少, 只是使用的时候有些限制: 1. 只有1个接受者; 2. 发送者一次发不完的时候不能block;
  • 任务A使用xTaskNotifyIndexed() xTaskNotifyGiveIndexed()发通知. 通知进入Pending态, 直到任务B调用 xTaskNotifyWaitIndexed() ulTaskNotifyTakeIndexed(). 函数名里面的Indexed可以去掉, 就对应的是Index==0的通知(感觉是为了保持版本的兼容性)
  • 通知可部分替代二进制信号量/计数信号量/事件组/邮箱机制. 官方文档对此有专门章节做示例

Stream & Message Buffers流和消息缓冲区

10.0.0以后才有的特性

TBD. 这么新的特性估计暂时用不到, 晚些在研究.

Software Timers软时钟

  • 是可选功能, 需要自行开启
  • 分一次性时钟和自重启时钟
  • 在时钟未溢出前还可以Reset时钟以重新计时

Event Bits (or flags) and Event Groups 事件位(标志)和事件组

  • 事件位指示事件是否发生, 一组事件位就是事件组
  • 可以在事件位变成1以前等待

静态和动态内存分配

  • 很多前面说的对象都可以动态或者静态分配内存. 动态分配就是在运行时再分配, 静态分配就是在link的时候就分配好,还能指定分配的地址.

heap堆管理

最新版本有5种堆管理方式.
这部分感觉暂时也用不上, 看原文吧.

栈溢出检查

新建项目

直到vTaskStartScheduler()调用后, 程序才表现得像个RTOS的样子.

调试

百度搜索了资料, 似乎只能通过打印信息调试, 搜索"FreeRTOS 断点调试"找不到结果.
Bing搜索"FreeRTOS breakpoint debug"还是能找到几条使用Keil设置断点调试的方式, 主要是说要在task里面设置断点, 并且不能用步入模式调试.
我设想一种调试方式, 即只针对task进行调试, 把task当做一个系统. 把FreeRTOS的API函数和硬件函数全部Stub来调试逻辑. 剩下的硬件逻辑和OS相关逻辑通过真机printf调试. 当然单元测试应该是可以用的.

芯片

主要系列是按CPU划分的.
其中ESP32和S2/S3/C3已经是不同的产品了, 这样命名挺讨厌, 自己既是大类的名称又是小类的名称...

芯片系列

系列 CPU
8266/8285 Xtensa L106
ESP32 Xtensa LX6 单核或双核
ESP32-S2 Xtensa LX7单核
ESP32-S3 Xtensa LX7双核
ESP32-C3 RISC-V

芯片后缀

后缀 意义
FHn n MB的flash
Rn n MB的PSRAM
FNn n MB的Flash

多年来都是瞎打,中午虽然有半小时出汗,也还想多获得一点儿乐趣。

提升命中率

视频: https://www.bilibili.com/video/BV1ux411L73q
要找到适合自己的舒服的投篮姿势. 每个人都是不同的.

  • 身体侧对篮筐而不要正对篮筐. 投篮侧的脚在前指向篮筐, 可减轻肩膀压力有利于命中.
  • 脚后跟不能着地而要脚尖着地跳投. 跳投前脚后跟接触地面会泄力. 脚尖着地也能增加射程
  • 跳投双脚前摆(而不是落在原地)能让出手点提高弧度提高从而提高命中率
  • 投篮手部姿势从起手位就要弯好,而不是要发力投球的时候再后弯手腕和准备其他手指
  • 下盘动作可以越快越好, 目标是最快速度踩到舒适投篮位置, 做好舒适投篮动作
  • 上盘动作要越舒展越好, 要保持固有节奏, 不应因为要着急快速投篮而肆意加快上盘动作, 导致投篮失准
  • 将球快速拉到自己舒适的起球垂面上, 藏于篮球口袋(大概意思就是在衣服下口袋的位置?), 并沿着垂直面向上起球. 同样不能因为着急而不沿着自己熟悉的投球线路起球.

总结: 投球要准就是要将身体准备姿势放置到最舒适位置才投.

AHK用了很久却一直没有深入学过, 今天深入一点写个入门.
AHK不比什么RPA工具效率低. 最大的缺失可能是元素识别.

五种语句体

;0. 自动执行的,跟js一样写什么执行什么
Some code
return

;1.定义快捷键(热键)的
^j::
  Some Code
  Return

;2. 定义热字符串的
::btw::
  Send, by the way
  return

;3. 定义子程序的. 相当于汇编里面的label, 然后用gosub调用(跟goto有点像)
subName:
  Some Code
  return

;4. 定义函数的
function(param1, param2:="xxx"){
  Some Code
  return "A value"
}

多文件组织

可以用#include包含其他ahk文件

传统语法

ahk说明里面讲到ahk历史悠久,所以有一些传统语法(就是看上去有些恶心的语法), 比如说某些命令里面字符串可以不加引号让你搞不清哪儿是字符串开始和结尾...

基础语法

; 单行注释
/*
多行注释
但有趣(烦躁)的是, /*和*/必须在行首. 像这一行的这俩是不起作用的
*/
"这是一个字符串, 需要双引号" 
变量 := 赋值

讲讲字符串里面的转义. C语言是用\转义, AHK比较变态使用<code></code>转义,搞得我markdown都得写code标签. 主要转义有%t rn. %用于创建动态变量引用和动态函数调用, 有点让我想起来.bat格式
但是但是但是, "的转义是"", 很神奇吧, 那就是说如果要表示一个引号字符串,加上把它括起来的引号实际上是4个引号哈哈哈哈""""

+-*/(), 和++ -- += -=等运算符都支持, condition?value1:value2的三元运算也支持.
:=是赋值运算符, 不太一样. and or not是逻辑运算符

支持类似js的对象, 可以用这样的obj := {Key1: Value1, Key2: Value2}形式创建, 以obj.Key1的形式访问
数组也是[]形式的, 但是第一个成员的序号是1而不是0

Ctrl键未绑定的快捷方式

按键:

软件 ABCDEFGHIJKLMNOPQRSTUVWXYZ 12346890 F123456789⒑⒒⒓
EXCEL JMQ F1 F2

Excel参考: https://www.asap-utilities.com/excel-tips-shortcuts.php

自带的常用ahk

Windowspy.ahk 用于查找句柄ClassNN

部分常用命令

; 设置鼠标坐标为屏幕
CoordMode, Mouse, Screen

; 按窗口标题名称点击登录
ControlClick, X161 Y489, 系统登录

; 点击坐标
Click, 123 456

; 激活不在前台的窗口
WinWaitActive, ahk_exe EXCEL.EXE

; 停1000ms
Sleep, 1000

; 发送alt+F4
Send, !{F4}

; 发送字符串并且不激活输入法
Send, {text}some string

; 运行程序
Run, "D:\Program Files\网店管家云端版\wdgjyun.exe"

; 创建函数
saveExcel(filename)
{
  ; 删除文件
  FileDelete, %filename%
  Sleep, 1000
  send, {F12}
  Sleep, 2000
  send, {text}%filename%
  Sleep, 1000
  send, {Enter}
  Sleep, 1000
  send, {Enter}
  Sleep, 2000
  Send, !{F4}
}

; 点击图片. x1,y1 ~ x2,y2是图片的搜索区域. delta是点击位置与图片左上角在xy坐标上的偏移位置
imageClick(x1, y1, x2, y2, imageFile, delta){
  ImageSearch, FoundX, FoundY, x1, y1, x2, y2, %imageFile%
  if (ErrorLevel = 2)
      MsgBox Could not conduct the search.
  else if (ErrorLevel = 1)
      MsgBox Icon could not be found on the screen.
  else{
      MouseClick, left, FoundX+delta, FoundY+delta
  }
}

Arduino方式开发

NonOS开发

FreeRTOS开发

ESP-AT开发

乐鑫已经停止了对8266 AT开发的支持, 后续只维护BUG. 现在乐鑫推荐 ESP32-C3也即是RISC-V平台.
ESP-AT Git项目能找到8266最后版本的技术文档

ESP-IDF开发

官方文档是这个: ESP8266 SDK的开发指南, 目前只有英文

CSDN上半颗心脏同学的8266开发笔记
乐鑫关于8266和32开发用IDF的区别, 8266 SDK从V3.0开始与IDF风格统一, 但由于不是完全兼容, 所以名称没有改为IDF.
ESP8266 SDK的GIT库
ESP-IDF的GIT库

环境搭建

还挺费劲, 下载的东西还挺多

准备工作

  1. 大多数人可能还是用windows 环境吧
  2. 需要下载3个包, 1个是msys2模拟linux环境, 1个是8266 编译器, 1个是8266项目包
  3. 需要用到github,所以准备好梯子

具体步骤

  1. 下载并解压 toolchain: https://dl.espressif.com/dl/esp32_win32_msys2_environment_and_toolchain-20181001.zip, 解压缩后把里面的msys32放到一个简短点儿的目录里比较好, 如D:\msys32
  2. 双击里面的mingw32.exe, 这就是以后的主要工作环境了. 先在home\用户名\也就是~目录下建立一个esp目录, 把开发支持工具(等会儿会有2个), 放在这儿. home目录就在D:\msys32下面.
  3. 下载第一个开发支持工具: esp8266 toolchain, https://dl.espressif.com/dl/xtensa-lx106-elf-gcc8_4_0-esp-2020r3-win32.zip, 解压缩到esp目录里面.
  4. mingw32里面用vim .bashrc增加一句话export PATH="$PATH:$HOME/esp/xtensa-lx106-elf/bin"
  5. esp目录里下载第二个支持工具. 这儿要用gitgithub. 所以你可能要先在.bashrc里面设置个代理export http_proxy="192.168.0.1:8080", 然后重启mingw32, 进入esp目录运行: git clone https://github.com/espressif/ESP8266_RTOS_SDK.git , 也可以下载zip包https://github.com/espressif/ESP8266_RTOS_SDK/releases/download/v3.4/ESP8266_RTOS_SDK-v3.4.zip直接解压, 然后把clone后的路径设置为系统变量IDF_PATH. 可以在.bashrc里面设置, 增加一行: export IDF_PATH="$HOME/esp/ESP8266_RTOS_SDK",也可以再windows系统的环境变量里面设置, 如果在winows环境变量设置, 值要设置为/home/用户名/esp/ESP8266_RTOS_SDK这样的形式. 似乎~符号不认. 重启mingw32使设置生效.
  6. 运行 python -m pip install --user -r $IDF_PATH/requirements.txt
  7. 复制一个hello world出来cp -r $IDF_PATH/examples/get-started/hello_world
  8. 进到hello_world目录运行make menuconfig, 出来个UI界面配置一下串口名称如COM18, 保存退出. 注意这个是ESP8266的烧录串口, 波特率74880
  9. 运行make flash, 会检查环境, 并且从github下载项目, 编译, 挺花时间, 没个十几分钟搞不定. 可以增加一个-j8参数, 使用8核编译,速度会快很多
  10. 运行make monitor可以监控串口输出.

好了, 看起来终于可以开始了.

其他有用的命令

make all 只编译而不烧录, 也会在最后显示烧录命令.
make print_flash_cmd 打印烧录命令, 可以用在其他地方
make partition_table 打印分区表, 一般分为两种: 非OTA的分区表Single factory app, no OTA和OTA分区表Two OTA app, 可以在make menuconfig中选择.

分区表

分区表烧录在0x8000位置, 长度为0xC00
非OTA的分区表:

# Espressif ESP8266 Partition Table
# Name,   Type, SubType, Offset,  Size
nvs,      data, nvs,     0x9000,  0x6000
phy_init, data, phy,     0xf000,  0x1000
factory,  app,  factory, 0x10000, 0xF0000

NVS是非易失存储, PHY似乎是物理什么的
OTA的分区表

# Espressif ESP8266 Partition Table
# Name,   Type, SubType, Offset,   Size
nvs,      data, nvs,     0x9000,   0x4000
otadata,  data, ota,     0xd000,   0x2000
phy_init, data, phy,     0xf000,   0x1000
ota_0,    0,    ota_0,   0x10000,  0xF0000
ota_1,    0,    ota_1,   0x110000, 0xF0000

可以看到OTA分区表是兼容非OTA分区表的, 兼容性明显比以前的8266项目好多了. 网上有很多介绍以前的8266分区表的, OTA分为user0/user1, user0起始于0x1000而不是现在的0x10000, 现在应该已经不适用了.
otadata是指示启动那个app的, 如果为空就启动ota_0
分区表可以自定义, 不过我感觉一般是没必要了

系统任务

大写T在最后面的是IDF的任务, 其他的是FREERTOS的任务.

uiT 用户初始化任务, 初始化完以后调用app_main再销毁自己
IDLE FREERTOS的空闲任务, 在其钩子vApplicationIdleHook中调用sleep和喂狗任务
Tmr FREERTOS的软时钟
ppT 处理WIFI硬件驱动,我猜是process protocol的缩写
ppT 系统电源管理, power managerment.
rtT 高优先级的硬时钟中断任务. 主要处理WIFI实时事件. 基于这个组件的程序不要在应用(application)里面调用,以免影响WIFI收发.
tiT Tcp-Ip协议栈的任务(LwIP), 处理TCP-IP包.
esp_event_loop_task 处理系统事件
优先级从低到高依次是:

0 IDLE 
2 Tmr 
8 tiT 
10 esp_event_loop_task
11 pmT
12 rtT
13 ppT
14 uiT

用户任务的优先级要低于rtT也就是12.
如果要加快 TCP/UDP 吞吐量,可以尝试将发送/接收任务的优先级设置为高于“tiT”任务的优先级(8)

PWM和嗅探器Sniffer共存问题

SmartConfig应该属于一种Sniffer.
8266是没有硬件PWM的, 它是用硬时钟模拟的软件PWM. 硬时钟同时用于WIFI, 所以WIFI sniffer和PWM同时用会出现资源争用的问题.
N个通道的PWM每次翻转GPIO将占用6+8N ns的时长. 例如一个通道PWM就是14ns.
占用期间, 如果收到了必须要实时处理的LPDC或HT40包, 这些包就会被丢弃.
同时使用PWM和smartconfig会使得配网时间更长. 而且调整PWM的频率/duty cycle/phase都会影响smartconfig的速度.
如果要同时使用, 需要:

  • PWM 的频率不能太高,最多 2KHz。
  • 修改PWM的占空比和相位,使每个通道反相之间的时间间隔(Tn)等于0或大于50us(Tn = 0,或Tn > 50)
    这样看起来这个软件PWM似乎不太适合用于LED驱动~~

使用API-Reference文档

非常神奇的, 8266仿照IDF风格的API编程手册, 乐鑫已经放弃治疗了, 8266 RTOS SDK 编程指南最后版本V3.4中, 居然只是把每个函数罗列了一遍, 有个球用. 找了网上各种指南, 发现只能参考ESP32的手册, 在同样的目录结构下, Uart的使用就有非常详细的描述.

使用 make menuconfig工具

感觉有些操蛋~ 很多在程序里面定义的参数还要再menuconfig里面预先定义一下, 否则程序编译后运行得总有些问题. 而且make menuconfig重新定义以后,这个编译速度就慢的惊人了

使用linux服务器或者使用docker中加载linux环境, make的速度可以快好几倍.

menuconfig主要是在根目录下生成sdkconfig文件作为编译依据. 同时会把旧的文件改名为sdkconfig.old. 如果熟练的情况可以直接修改sdkconfig文件.

是否要重新编译, 似乎是根据sdkconfig有没有更新决定的. 所以即使修改了一个和编译完全无关的项目, 如PC烧录串口, 也会导致全部重新编译.

IDF版本兼容性

IDF各个版本兼容性不佳, 如8266 v3.4和v3.1是不兼容的. 发现的就有 uart_driver_install在v3.4比v3.1多一个参数,导致编译不能通过.

8266的串口输出

8266只有1.5个串口, 也就是1个完整串口UART0和1个只有TX的串口. 所谓有时候看到的UART2(IO13/15)是UART0交换(swap)过去的.

基于idf, 函数中直接使用printf函数将直接从uart0口输出, 不需要串口初始化的步骤.

  • 在menuconfig中将Swap UART0 I/O pins选上, 编译烧录后, 刚刚启动打印的boot信息在UART0输出, 后面printf的信息都在UART2上输出. 如果使用了软重启esp_restart();, 软重启后的boot信息也将在UART2上输出.

  • 在menuconfig中将Uart for console output选项改为Custom, 如果UART peripheral to use for console output (0-1)仍然为UART0, 结果和上面的一样不变.

  • 如果将UART peripheral to use for console output (0-1)改为UART1, 那么第一次启动的boot信息在UART0输出, printf的信息在UART1输出, 软重启后的boot信息在UART2输出.

  • 在meunconfig中将Partition Table改为Factory app, two OTA definitions不会对运行结果有什么影响, 但是flash烧录的固件名称会改为partitions_two_ota.bin
    如果是单文件, 烧录命令是(project_template是项目的目录名):

    D:\r\esp\esp-idf\components\esptool_py\esptool\esptool.py --chip esp8266 --port COM18 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 2MB 0x0000 build/bootloader/bootloader.bin 0x10000 build/project_template.bin 0x8000 build/partitions_singleapp.bin

    如果是ota,烧录命令是:

    D:\r\esp\esp-idf\components\esptool_py\esptool\esptool.py --chip esp8266 --port COM18 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 2MB 0x0000 build/bootloader/bootloader.bin 0x10000 build/project_template.bin 0x8000 build/partitions_two_ota.bin
  • menuconfig只能控制所谓console的uart设置, 如果要使用uart,还是要自己通过程序进行uart初始化设置.

  • uart初始化使用uart0的影子uart2, 如果在menuconfig中已经设置了swap, 在初始化中就不需要再次调用swap函数了.

    static void uartInit()
    {
      // Configure parameters of an UART driver,
      // communication pins and install the driver
      uart_config_t uart_config = {
          .baud_rate = 115200,
          .data_bits = UART_DATA_8_BITS,
          .parity    = UART_PARITY_DISABLE,
          .stop_bits = UART_STOP_BITS_1,
          .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
      };
      uart_param_config(UART_NUM_0, &uart_config);
      uart_driver_install(UART_NUM_0, BUF_SIZE * 2, 0, 0, NULL);
    }
  • v3.1printf的问题: 有时候不会及时输出, 尤其是在使用了格式化符号%d%x等等后, 必须要用fflush(stdout)后才能输出.

include顺序

idf头文件#include是有顺序的,否则编译不通过.
感觉顺序似乎应该是标准库文件->freertos头文件->esp系统文件->驱动文件

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"
#include "driver/uart.h"

  1. 签名要用Adobe Reader XI 11.07版本. 不多不少.
  2. Adobe Reader XI 已经被Adobe官方停更了, 也就是在官网下载不到咯, 目前官网最新版是 Reader DC.
  3. 但你不能用Reader DC, 因为很多CA证书仍然是使用SHA1加密,但Reader DC因为其不安全已经不支持了;
  4. 好吧,其实DC是支持的,只不过要改注册表,然而他还不推荐你改,就不好好给你提供个地址让你去改;
  5. 然后你还是要退回到 Reader XI 11.07版本, 安装的时候记得所有都要点同意。杀软要是拦截一定得放行了,否则你会在签名的时候遇到奇妙的闪退问题;
  6. 是的,你不得不用不安全的落后的CA证书和已经被弃用的Reader版本来完成签名。X蛋不?

最近用一款51单片机与8266(AT固件)的UART2接口TTL串口通讯, 使用115200的波特率总是每70多个字节或100多个字节出现误码, 8266使用的WROOM-02D模块, 26MHz晶振, 而51使用的是RC震荡.
具体误码的情况是: 8266发送到51不会误码, 而51发送给8266就会误码. 使用电脑HTerm查看, 8266只要启动了AT固件, 51发送的就会误码, 8266不正常启动(如进入烧录模式)或者启动一个啥也不干的空程序, 就不会误码. 怀疑是8266初始化UART2串口以后才有的问题.
尝试了如下各种方法都失败了:

  • 尝试将51的波特率降低一半到57600, 仍然误码.
  • 尝试将51的波特率改一点点如115201, 结果根本就读不到数据, 这不科学! 难道是硬件串口内置了集中固定的波特率? 因为波特率都需要时钟发生器, 这倒是很有可能无法改一点点.
  • 尝试电脑端更改波特率, 发现大约在(115200-3000, 115200+6000)左右都能正确接收, 超出一点儿之后会出现误码,超出太多就全部乱码了. 算下来容忍度有8%左右.

参考了这篇文章, 里面讲到一次不要发的太多. 考虑到每次发送都是在相对固定的位置误码(70个字符以后), 最后成功的方法是:

每次发送50个字符后, 延时5ms再继续发送.

延时时长是反复测试得出的.测试发现每发送50字符延时2.5ms正好可以不误码, 为了容错将这个时间加大一倍为5ms.
硬件上继续尝试解决, 看是否能够找到最终原因.

久了不用就会忘
参考: https://zhuanlan.zhihu.com/p/85383698

  • A的所有B后代: .A .B{}
  • A的所有B子代: .A>.B{}
  • A后面一个邻居: .A+.B{}
  • A所有邻居: .A~.B{}
  • A和B的交集: AB{}, 选择这样的元素: <div class="A B"></div>
  • A和B的并集: A,B{}, 选择这样的元素 <div class="A"></div><div class="B"></div>

b站这个9分钟教程很好https://www.bilibili.com/video/BV12J411z7j7?p=1

原理图

基本操作

  • 新建工程(图标:左上角的本子)
  • 新建原理图(原理图编辑器)
  • 图框设置(图标:一张纸上有个坐标轴): 设置信息
  • A键, 选择元器件, 其中: C是电容, R是电阻, 一般的接口在Connector_generic中, USB在connector中
  • 放好元器件, 按M移动并调整位置, R旋转, X垂直镜像, Y水平镜像
  • P放置电源和地线. 如5VGND. 对于电源和地线,还需要选择两个PWR_FLAG并分别连接到电源和地线上
  • W键进行连线
  • C可以复制当前指向的元器件
  • 直接左键框选可移动框选住的目标, 按Shift+左键框选可以复制框选目标并移动
  • 连线后按M键移动元器件, 连线不会跟着动, 这是需要按G键, 可以带着线移动元器件
  • V键可以修改元器件的值, 按E键也可以改
  • L键可以为线增加标签, 相同标签的线认为是连在一起的

自动序号

标注原理图(图标: 一张纸上一个铅笔), 点击标注就可以给元器件自动上符号

设置封装

编辑符号字段(图标: 一张表格), 在对应的元器件的Footprint栏里面, 点击后面的书架样子的图标, 打开封装库, 选择封装. 选择完封装才能做PCB.

检查

点击执行电器规则检查(图标: 瓢虫), 点击运行即可以看到检查到的问题.
修改后再次检查直到没有问题.

PCB

在原理图上按F8键, 点击更新PCB,即进入PCB编辑器, 鼠标上带着所有元器件. 单击放置.

基本操作

  • 设置原点. 点击右侧设置网格原点(图标: 4x4个点带1个红点), 选择要画的PCB的左下角
  • 在右侧的中选择Edge.Cuts即PCB刀切层, 选择后会有个小三角指向这个层
  • 点击添加图形线条(图标: 4个点3条线连着), 在绘图区点击右键, 选择网格中合适的网格大小, 如1.0mm, 从上面的原点开始画图, 画一圈边线连接起来.
  • Margin层, 画在刀切层内0.3mm画一圈布线区域. 不能超过这个区域布线以免被刀切断
  • 在元器件上按M键可以移动, R键旋转. 但是XY是用不了的. X另有它用
  • 在元器件上双击, 弹窗中可以修改坐标数值精确定位
  • 放好器件后, F.FabB.Fab层可能干扰实现, 可以关闭.

布线

  • 布线前先定义线. 点击电路板设置(图标: 电路板上一个齿轮), 按需要设置设计规则, 网络类表, 导线和过孔, 阻焊/锡膏
  • 一对线的走法可以按Alt+6选择差分对布线, 选择后点击右键选择差分对标注并选择定义好的差分对值, 然后给一对管脚布线
  • 在管脚上点击X键可以进行单线布线. 布线的宽度可以在右上角下拉菜单里选择
  • 在布线中可以点击/键修改布线的方向, 在水平/垂直/45度方向间切换
  • 在布线中可以点击V键增加过孔
  • 布线后, 点击V键可以看背面的线
  • 布线后, 点击一条线后点击I键可以选择所有相连的线
  • 布线后, 点击D键可以不中断线的情况下平移线
  • 最后除了GND以外, 所有的线都布好
  • 点击Alt+3可以看3D图

标注和覆铜

(对应第三节: 出Gerber文件)

  • 选择F.SilkS层, 再点击添加标注(图标: 测距符号上有个N), 然后点击两个点就可以标注距离
  • 点击F.Cu层, 再点击添加填充区域(图标: 绿色底上有条过孔线), 点击覆铜区域的一个角, 弹出窗口, 选择合适的值, 确定后, 把区域画完,即可.
  • 在一个角上点右键选择覆铜->重复区域区域到层, 弹窗中选择B.Cu就可以给背面覆铜

官方原文: https://webpack.docschina.org/concepts/
Webpack似乎已经到了不得不深入学习的程度了, 要不然有的概念看不明白. 现在居然还有绕过npm install, 用webpack来引入外部库的方式!(PS: 通过后面的学习, 发现webpack确实可以在externals中声明CDN地址的方式来引入外部包)

Webpack是一个静态打包工具. 也就是从各种依赖里把需要的抽取出来, 形成bundle. bundle在webpack里就是的意思, 也就是"打包"的"包"
所有的配置都是在webpack.config.js中完成的
下面说的react和vue都指的是其cli工具

entry

是入口, 也被称之为chunk块. 默认是./src/index.js , 在react中是app.jsx,在vue中是./src/main.js,

output

是输出的bundle和其他文件的存储位置. 默认是./dist/main.js, 文件位置在dist文件夹中. react和vue都没有改这个文件夹.

loader

是处理除了.js.json文件打包以外, 其他文件的打包工具. 包括.css, .txt等等. loader是在module.rules中以数组的方式规定的. 每个数组元素包括以正则定义的后缀名test(不知道为啥叫test),和loader名.

plugin

执行除了loader以外的更宽泛任务. plugin需要通过npm install安装后在webpack.config.js前面require进来,然后在plugins下面定义数组元素.

mode

有三种值: production, development, none, 是对开发环境的定义

模块的导入方式

所有导入方式都需要预先npm install这个模块

  • 静态导入, 就是常规的在文件开始import.
  • 动态导入, 在使用模块的函数中, 先使用import导入(异步方式), 再使用模块.
  • 预获取prefetch. 提前动态获取下一个页面要用的模块. 这是通过在js的注释/**/中添加webpack命令实现的.
  • 预加载preload. 在加载本页面前先加载指定模块. 与上面同样是在js注释中实现.

外部化 externals

在创建library时尤其需要关联外部库, 而不是把外部库也打包到自己的库里面. 这儿就是使用externals. 使用了externals之后, 打包bundle就会把import的外部库排除掉.

为生产环境配置不同的config文件

本来嘛, 默认的config文件是webpack.config.js,然而为了生产的打包文件更小, 开发的打包文件更方便/热更新等, 就需要配置两个不同的config文件webpack.prod.js, webpack.dev.js, 然后为了把两个不同的config中共同部分提取公约数, 又有了第三个公共文件webpack.common.js.
另外我又发现在react的项目中, package.json又有一种用一个webpack.config.js实现两种环境的方式. 区分时加上不同的命令行参数:

"scripts": {
    "dev": "webpack-dev-server --config webpack/webpack.config.js --env.mode=development --watch",
    "release": "webpack --config webpack/webpack.config.js --env.mode=production",
    "dev:dualmode": "npm run dev -- --demo=DualmodePanel",
    "release:dualmode": "npm run release -- --demo=DualmodePanel",
    "dev:wugan": "npm run dev -- --demo=NonInductiveDemo",
    "release:wugan": "npm run release -- --demo=NonInductiveDemo"
  },

externals

这个属性只是为了不让webpack去打包指定的内容. 然而在开发时, 这个内容还是要能访问到的. 不管是用npm install,或者在index.html里面用<script>引入, 还是说在externals里面直接指定CDN地址.

原文

语言服务扩展包指南

正如你在《编程语言特性》章节看到的那样,我们可以直接使用languages.* API实现语言特性。而语言服务扩展包提供另一种方式实现对编程语言的支持。
本章节将:

  • 说明语言服务扩展包的特性
  • 带你应用Microsoft/vscode-languageserver-node库实现一个语言服务。你也可以直接在lsp-sample中浏览代码。

为什么使用语言服务?

语言服务是VS Code的一类特殊扩展包,可以增强多种编程语言的编辑体验。使用语言服务,你可以实现自动完成、错误检查(诊断)、转到定义和其他许多VS Code支持的语言功能。
然而,实现对这些语言功能的支持时,我们发现了三个常见问题:
第一,语言服务一般是以其原生语言实现,而在以Node.js为运行时的VS Code中集成它们却是个挑战。
其次,语言特性可能极耗资源。例如,要正确地验证一个文件,语言服务需要解析一大堆的文件,建立抽象语法树(AST),实施静态程序分析。这些操作可能导致CPU和内存的大量占用,而同时我们还要保证VS Code的性能不受影响。
最后,在多种代码编辑器中集成多种语言工具可能导致巨大负担。对语言工具来说,它们需要为代码编辑器适配不同的API。而对代码编辑器来说,他们却不能期望语言工具能提供统一的API。这使得要对M种语言在N种代码编辑器中实现语言支持的话,工作量变成了M*N倍。
要解决这些问题,微软定义了语言服务协议LSP,将语言工具和代码编辑之间的通讯标准化。此时,当语言服务使用LSP与代码编辑器通讯时,它可以用任何语言实现,在其自身进程中执行,避免性能消耗。进一步,任何兼容LSP的语言工具都可以和多种兼容LSP的语言编辑器集成,当然任何兼容LSP的语言编辑器也很容易选择多种兼容LSP的语言工具。LSP能让语言工具提供者和代码编辑器提供者获得双赢!

本指南中,我们将:

  • 解释如何使用提供的Node SDK,在VS Code中建立语言服务扩展包
  • 解释如何运行、调试、记录日志和测试语言服务扩展包
  • 带给您一些语言服务的高级内容

实现语言服务

概述

在VS Code中, 语言服务由两部分组成:

  • 语言客户端: 用JavaScript/TypeScript写的普通VS Code扩展包.这个扩展包能访问所有VS Code的命名空间API
  • 语言服务端:独立进程运行的语言分析工具。

如上面所提到的,独立进程的语言服务端有两个好处:

  • 分析工具可以用用任何语言实现,同时它可与语言客户端依照语言服务协议通讯。
  • 语言分析工具通常带来CPU和内存的高占用,在独立进程运行可以避免性能损耗。
    下图说明VS Code如何运行两个语言服务扩展包。HTML语言客户端和PHP语言客户端是用Typescript写的普通VS Code扩展包。他们实例化相应的语言服务端并且与之通过LSP通讯。虽然PHP语言服务端是PHP写的,它依然可以通过LSP和PHP语言客户端通讯。

    本指南将叫你如何使用Node SDK构建语言客户端/服务端。下面部分将假定您已经对VS Code扩展包API很熟悉了。

LSP示例:纯文本文件的简单语言服务端

让我们对纯文本文件构建一个简单的语言服务端,实现自动完成和诊断功能。我们还将介绍客户端/服务端之间的配置同步。
如果您更喜欢直接看代码:

  • lsp-sample:本指南的源代码完整存档。
  • lsp-multi-server-sample:lsp-sample 高级版的源码存档,它为每个工作区文件夹启动一个单独的服务端实例,以支持 VS Code 中的多根工作区功能。
    Clone Microsoft/vscode-extension-samples库,打开示例:
    > git clone https://github.com/microsoft/vscode-extension-samples.git
    > cd vscode-extension-samples/lsp-sample
    > npm install
    > npm run compile
    > code .

    以上安装所有依赖项并打开包含客户端和服务端代码的 lsp-sample 工作区。以下是lsp-sample结构的概览:

    .
    ├── client // Language Client
    │   ├── src
    │   │   ├── test // End to End tests for Language Client / Server
    │   │   └── extension.ts // Language Client entry point
    ├── package.json // The extension manifest
    └── server // Language Server
      └── src
          └── server.ts // Language Server entry point

    语言客户端说明

    我们先来看看/package.json,它描述了语言客户端的能力。有三个有趣的部分:
    首先看activationEvents

    "activationEvents": [
      "onLanguage:plaintext"
    ]

    这部分告诉 VS Code 在打开纯文本文件(例如扩展名为 .txt 的文件)后立即激活扩展。

接下来看configuration部分:

"configuration": {
    "type": "object",
    "title": "Example configuration",
    "properties": {
        "languageServerExample.maxNumberOfProblems": {
            "scope": "resource",
            "type": "number",
            "default": 100,
            "description": "Controls the maximum number of problems produced by the server."
        }
    }
}

本节为 VS Code 提供configuration设置。该示例将解释如何在启动时和每次更改设置时将这些设置发送到语言服务端。

语言客户端实际代码和相应的 package.json 位于 /client 文件夹中。 /client/package.json 文件中有趣的部分是它通过engines 字段引用了vscode 扩展包主API,并向vscode-languageclient 库添加了一个依赖项:

"engines": {
    "vscode": "^1.52.0"
},
"dependencies": {
    "vscode-languageclient": "^7.0.0"
}

如前所述,客户端是作为普通的 VS Code 扩展包实现的,它可以访问所有 VS Code 命名空间 API。

下面是对应的extension.ts文件的内容,也是lsp-sample扩展的入口:

import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';

import {
  LanguageClient,
  LanguageClientOptions,
  ServerOptions,
  TransportKind
} from 'vscode-languageclient/node';

let client: LanguageClient;

export function activate(context: ExtensionContext) {
  // The server is implemented in node
  let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
  // The debug options for the server
  // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
  let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };

  // If the extension is launched in debug mode then the debug server options are used
  // Otherwise the run options are used
  let serverOptions: ServerOptions = {
    run: { module: serverModule, transport: TransportKind.ipc },
    debug: {
      module: serverModule,
      transport: TransportKind.ipc,
      options: debugOptions
    }
  };

  // Options to control the language client
  let clientOptions: LanguageClientOptions = {
    // Register the server for plain text documents
    documentSelector: [{ scheme: 'file', language: 'plaintext' }],
    synchronize: {
      // Notify the server about file changes to '.clientrc files contained in the workspace
      fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
    }
  };

  // Create the language client and start the client.
  client = new LanguageClient(
    'languageServerExample',
    'Language Server Example',
    serverOptions,
    clientOptions
  );

  // Start the client. This will also launch the server
  client.start();
}

export function deactivate(): Thenable<void> | undefined {
  if (!client) {
    return undefined;
  }
  return client.stop();
}

语言服务端说明

注意:从 GitHub 克隆的“服务端”为练习的最终实现效果。要遵循练习步骤,您可以创建新的 server.ts 或修改克隆版本的内容。

在示例中,服务端也是用 TypeScript 实现的,并使用 Node.js 执行。由于 VS Code 已经附带了 Node.js 运行时,因此无需提供您自己的运行时,除非您对运行时有特定要求。

语言服务端的源代码位于 /server。服务端的 package.json 文件中有趣的部分是:

"dependencies": {
    "vscode-languageserver": "^7.0.0",
    "vscode-languageserver-textdocument": "^1.0.1"
}

这会引入 vscode-languageserver 库。

下面是一个服务端实现,它使用简单的文本文档管理器,其同步文档的方式为:总是将文件的完整内容从 VS Code 发送到服务器。

import {
  createConnection,
  TextDocuments,
  Diagnostic,
  DiagnosticSeverity,
  ProposedFeatures,
  InitializeParams,
  DidChangeConfigurationNotification,
  CompletionItem,
  CompletionItemKind,
  TextDocumentPositionParams,
  TextDocumentSyncKind,
  InitializeResult
} from 'vscode-languageserver/node';

import { TextDocument } from 'vscode-languageserver-textdocument';

// Create a connection for the server, using Node's IPC as a transport.
// Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all);

// Create a simple text document manager.
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

let hasConfigurationCapability: boolean = false;
let hasWorkspaceFolderCapability: boolean = false;
let hasDiagnosticRelatedInformationCapability: boolean = false;

connection.onInitialize((params: InitializeParams) => {
  let capabilities = params.capabilities;

  // Does the client support the `workspace/configuration` request?
  // If not, we fall back using global settings.
  hasConfigurationCapability = !!(
    capabilities.workspace && !!capabilities.workspace.configuration
  );
  hasWorkspaceFolderCapability = !!(
    capabilities.workspace && !!capabilities.workspace.workspaceFolders
  );
  hasDiagnosticRelatedInformationCapability = !!(
    capabilities.textDocument &&
    capabilities.textDocument.publishDiagnostics &&
    capabilities.textDocument.publishDiagnostics.relatedInformation
  );

  const result: InitializeResult = {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      // Tell the client that this server supports code completion.
      completionProvider: {
        resolveProvider: true
      }
    }
  };
  if (hasWorkspaceFolderCapability) {
    result.capabilities.workspace = {
      workspaceFolders: {
        supported: true
      }
    };
  }
  return result;
});

connection.onInitialized(() => {
  if (hasConfigurationCapability) {
    // Register for all configuration changes.
    connection.client.register(DidChangeConfigurationNotification.type, undefined);
  }
  if (hasWorkspaceFolderCapability) {
    connection.workspace.onDidChangeWorkspaceFolders(_event => {
      connection.console.log('Workspace folder change event received.');
    });
  }
});

// The example settings
interface ExampleSettings {
  maxNumberOfProblems: number;
}

// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings: ExampleSettings = { maxNumberOfProblems: 1000 };
let globalSettings: ExampleSettings = defaultSettings;

// Cache the settings of all open documents
let documentSettings: Map<string, Thenable<ExampleSettings>> = new Map();

connection.onDidChangeConfiguration(change => {
  if (hasConfigurationCapability) {
    // Reset all cached document settings
    documentSettings.clear();
  } else {
    globalSettings = <ExampleSettings>(
      (change.settings.languageServerExample || defaultSettings)
    );
  }

  // Revalidate all open text documents
  documents.all().forEach(validateTextDocument);
});

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
  if (!hasConfigurationCapability) {
    return Promise.resolve(globalSettings);
  }
  let result = documentSettings.get(resource);
  if (!result) {
    result = connection.workspace.getConfiguration({
      scopeUri: resource,
      section: 'languageServerExample'
    });
    documentSettings.set(resource, result);
  }
  return result;
}

// Only keep settings for open documents
documents.onDidClose(e => {
  documentSettings.delete(e.document.uri);
});

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
  validateTextDocument(change.document);
});

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

connection.onDidChangeWatchedFiles(_change => {
  // Monitored files have change in VS Code
  connection.console.log('We received a file change event');
});

// This handler provides the initial list of the completion items.
connection.onCompletion(
  (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // The pass parameter contains the position of the text document in
    // which code complete got requested. For the example we ignore this
    // info and always provide the same completion items.
    return [
      {
        label: 'TypeScript',
        kind: CompletionItemKind.Text,
        data: 1
      },
      {
        label: 'JavaScript',
        kind: CompletionItemKind.Text,
        data: 2
      }
    ];
  }
);

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
  (item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
      item.detail = 'TypeScript details';
      item.documentation = 'TypeScript documentation';
    } else if (item.data === 2) {
      item.detail = 'JavaScript details';
      item.documentation = 'JavaScript documentation';
    }
    return item;
  }
);

// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);

// Listen on the connection
connection.listen();

添加一个简单的验证

为了向服务端添加文档验证,我们向文本文档管理器添加了一个侦听器,每当文本文档的内容发生更改时都会调用该侦听器。然后由服务器决定验证文档的最佳时间。在示例实现中,服务器验证纯文本文档并标记所有使用全部大写的单词。相应的代码片段如下所示:

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(async change => {
  let textDocument = change.document;
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
});

诊断提示和技巧

  • 如果开始和结束位置相同,VS Code 将在该位置用波浪线下划线。

  • 如果您想用波浪线下划线直到行尾,则将结束位置的字符设置为 Number.MAX_VALUE。

要运行语言服务端,请执行以下步骤:

  • Ctrl+Shift+B 开始构建任务。该任务编译客户端和服务端。

  • 打开 Run 视图,选择 Launch Client 启动配置,然后按 Start Debugging 按钮启动执行扩展代码的 VS Code 的 Extension Development Host 实例。

在根文件夹创建一个test.txt文件,粘贴如下内容:

TypeScript lets you write JavaScript the way you really want to.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
ANY browser. ANY host. ANY OS. Open Source.

Extension Development Host 实例将如下所示:

调试客户端和服务端

陶氏客户端代码和调试普通扩展包一样容易,在客户端代码中设置断点,按F5调试扩展包。

由于服务端是由运行在扩展包(客户端)中的LanguageClient 启动的,因此我们需要将调试器附加到正在运行的服务器上。为此,切换到Run视图并选择启动配置Attach to Server并按 F5。这会将调试器附加到服务端。

语言服务的日志支持

如果你使用 vscode-languageclient 来实现客户端,你可以指定[langId].trace.server 来要求客户端把其与服务器之间的通信记录日志记录到,客户端name通道中。
对于 lsp-sample,您可以设置此设置:"languageServerExample.trace.server": "verbose"。然后切换到Language Server Example通道。您应该会看到如下日志:

在服务端使用配置

在编写扩展的客户端部分时,我们已经定义了一个设置来控制最大报告问题数。我们还在服务端编写了代码来从客户端读取这些设置:

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
  if (!hasConfigurationCapability) {
    return Promise.resolve(globalSettings);
  }
  let result = documentSettings.get(resource);
  if (!result) {
    result = connection.workspace.getConfiguration({
      scopeUri: resource,
      section: 'languageServerExample'
    });
    documentSettings.set(resource, result);
  }
  return result;
}

我们现在唯一需要做的就是监听服务端的配置更改,如果设置发生更改,则重新验证打开的文本文档。为了能够重用文档更改事件处理的验证逻辑,我们将代码提取到 validateTextDocument 函数中并修改代码以支持 maxNumberOfProblems 变量:

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

配置更改的处理是通过将配置更改的通知处理程序添加到connection来完成的。对应的代码如下所示:

connection.onDidChangeConfiguration(change => {
  if (hasConfigurationCapability) {
    // Reset all cached document settings
    documentSettings.clear();
  } else {
    globalSettings = <ExampleSettings>(
      (change.settings.languageServerExample || defaultSettings)
    );
  }

  // Revalidate all open text documents
  documents.all().forEach(validateTextDocument);
});

再次启动客户端并将最大报告问题数更改为1,会导致以下验证结果:

添加额外的语言功能

语言服务通常实现的第一个有趣的功能是文档验证。从这个意义上说,即使是 linter 也算作语言服务,在 VS Code 中,linter 通常被实现为语言服务(参见 eslintjshint 示例)。但是语言服务还有更多。它们可以提供代码完成、查找所有引用或转到定义。下面的示例代码向服务端添加了代码完成功能。它提出了“TypeScript”和“JavaScript”这两个词。

// This handler provides the initial list of the completion items.
connection.onCompletion(
  (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // The pass parameter contains the position of the text document in
    // which code complete got requested. For the example we ignore this
    // info and always provide the same completion items.
    return [
      {
        label: 'TypeScript',
        kind: CompletionItemKind.Text,
        data: 1
      },
      {
        label: 'JavaScript',
        kind: CompletionItemKind.Text,
        data: 2
      }
    ];
  }
);

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
  (item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
      item.detail = 'TypeScript details';
      item.documentation = 'TypeScript documentation';
    } else if (item.data === 2) {
      item.detail = 'JavaScript details';
      item.documentation = 'JavaScript documentation';
    }
    return item;
  }
);

data字段用于唯一标识解析处理程序中的完成项。data属性对于协议是透明的。由于底层消息传递协议是基于 JSON 的,因此 data 字段应该只保存可序列化为JSON 或从 JSON 序列化的数据。
剩下的就是告诉 VS Code, 服务端支持代码完成请求的功能。为此,请在初始化处理程序中标记相应的功能:

connection.onInitialize((params): InitializeResult => {
    ...
    return {
        capabilities: {
            ...
            // Tell the client that the server supports code completion
            completionProvider: {
                resolveProvider: true
            }
        }
    };
});
下面的屏幕截图显示了在纯文本文件上运行的已完成的代码:
![](https://code.visualstudio.com/assets/api/language-extensions/language-server-extension-guide/codeComplete.png)

### 测试语言服务
要创建高质量的语言服务,我们需要构建一个良好的测试套件,涵盖其功能。有两种常见的测试语言服务方式:
* 单元测试:通过mock up发送所有信息,来在语言服务中测试特定功能。 VS Code的HTML / CSS / JSON语言服务采用这一方法进行测试。 LSP NPM模块也使用这一方法。使用NPM协议模块编写单元测试,请参阅[这儿](https://github.com/microsoft/vscode-languageserver-node/blob/main/protocol/src/node/test/connection.test.ts)。
* 端到端测试:这类似于[VS Code扩展包测试](https://code.visualstudio.com/api/working-with-extensions/testing-extension)。这种方法的好处是,它运行测试的方式是:使用工作空间实例化VS Code实例,打开文件,激活语言客户端/服务端,运行[VS Code命令](https://code.visualstudio.com/api/references/commands)。如果你有难以mockup的文件、设置或依赖关系(例如Node_Modules),则此方法更好。流行的Python扩展包采用这种方法进行测试。

你可以在选择的任何测试框架中进行单元测试。在这里,我们介绍了如何为语言服务扩展包进行端到端测试。
打开`.vscode / launch.json`,您可以找到`E2E`测试目标:
```ts
{
  "name": "Language Server E2E Test",
  "type": "extensionHost",
  "request": "launch",
  "runtimeExecutable": "${execPath}",
  "args": [
    "--extensionDevelopmentPath=${workspaceRoot}",
    "--extensionTestsPath=${workspaceRoot}/client/out/test/index",
    "${workspaceRoot}/client/testFixture"
  ],
  "outFiles": ["${workspaceRoot}/client/out/test/**/*.js"]
}

如果您运行这个调试目标,它将使用Client / TestFixture启动VS code实例作为活动工作区。然后,VS Code将继续执行client/ src / test中的所有测试。有个调试技巧,您可以在Client / SRC / Test中的Typescript文件中设置断点,并执行到断点处。
让我们来看看completion.test.ts文件:

import * as vscode from 'vscode';
import * as assert from 'assert';
import { getDocUri, activate } from './helper';

suite('Should do completion', () => {
  const docUri = getDocUri('completion.txt');

  test('Completes JS/TS in txt file', async () => {
    await testCompletion(docUri, new vscode.Position(0, 0), {
      items: [
        { label: 'JavaScript', kind: vscode.CompletionItemKind.Text },
        { label: 'TypeScript', kind: vscode.CompletionItemKind.Text }
      ]
    });
  });
});

async function testCompletion(
  docUri: vscode.Uri,
  position: vscode.Position,
  expectedCompletionList: vscode.CompletionList
) {
  await activate(docUri);

  // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion
  const actualCompletionList = (await vscode.commands.executeCommand(
    'vscode.executeCompletionItemProvider',
    docUri,
    position
  )) as vscode.CompletionList;

  assert.ok(actualCompletionList.items.length >= 2);
  expectedCompletionList.items.forEach((expectedItem, i) => {
    const actualItem = actualCompletionList.items[i];
    assert.equal(actualItem.label, expectedItem.label);
    assert.equal(actualItem.kind, expectedItem.kind);
  });
}

在这个测试中,我们:

  • 激活扩展包
  • 使用URI和位置运行命令vscode.executeCompletionItemprovider,来模拟完成触发。
  • 根据我们预期的完成项目断言返回的完成项目。
    让我们深入了解activate(docURI)功能。它是在client/ src / test / helper.ts中定义的:
import * as vscode from 'vscode';
import * as path from 'path';
export let doc: vscode.TextDocument;
export let editor: vscode.TextEditor;
export let documentEol: string;
export let platformEol: string;

/**
 * Activates the vscode.lsp-sample extension
 */
export async function activate(docUri: vscode.Uri) {
  // The extensionId is `publisher.name` from package.json
  const ext = vscode.extensions.getExtension('vscode-samples.lsp-sample')!;
  await ext.activate();
  try {
    doc = await vscode.workspace.openTextDocument(docUri);
    editor = await vscode.window.showTextDocument(doc);
    await sleep(2000); // Wait for server activation
  } catch (e) {
    console.error(e);
  }
}

async function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

在激活部分中,我们:

  • 使用{publisher.name}.{extensionId}获取扩展, 并在package.json中定义。
  • 打开指定的文档,并在活动文本编辑器中显示。
  • 睡眠2秒,所以我们确保语言服务已经实例化。

准备完成后,我们可以运行与每个语言功能相对应的VS Code命令,并根据返回的结果断言。
另外还有一个涵盖我们刚刚实施的诊断功能的测试, 可以查看client/ src / test / diagnostics.test.ts文件。

高级主题

到目前为止,本指南涵盖:

  • 语言服务和语言服务协议的简要概述。
  • VS Code中语言服务扩展包的体系结构
  • lsp-Sample扩展,以及如何开发/调试/检查/测试它。
    有一些更高级的主题不太适合在本指南中叙述。我们将在进一步学习语言服务开发时,给出这些资源的链接。

其他语言服务的功能

语言服务器目前支持以下语言功能以及代码完成:

  • 文档高亮:突出显示文本文档中的所有“相等”符号。
  • 悬停:为在文本文档中选择的符号提供悬停信息。
  • 签名帮助:为文本文档中选择的符号提供签名帮助。
  • 转到定义:为文本文档中选择的符号提供转到定义的支持。
  • 转到类型定义:为文本文档中选择的符号提供转到类型/接口定义的支持。
  • 转到实现:为文本文档中选择的符号提供转到实现定义的支持。
  • 查找引用:查找在文本文档中选择的符号的所有项目范围引用。
  • 列出文档符号:列出文本文档中定义的所有符号。
  • 列表工作区符号:列出所有项目范围的符号。
  • 代码操作:计算要为给定文本文档和范围运行的命令(通常是美化/重构)。
  • 代码长度Codelens:计算给定文本文档的代码长度统计信息。
  • 文档格式化:这包括格式化的整个文档,部分文档和按类型格式化。
  • 重命名:项目范围的符号重命名。
  • 文档链接:计算和解析文档中的链接。
  • 文档颜色:在文档中计算和解析颜色, 并编辑器中提供取色器。

程序语言功能主题描述了上面的每个语言功能,并指导你通过语言服务协议、或直接从你的扩展包中使用可扩展性API,来实现它们。

增量文本文档同步

根据知乎文章https://zhuanlan.zhihu.com/p/48968584, 对于一项著作权的估值, 就是三种方式.
一是看做这个花了多少钱, 二是看别人做的卖多少钱, 三是看自己做的能卖多少钱.
话说回来, 这三种方法似乎也适用于其他一切东西的估值.