zoukankan      html  css  js  c++  java
  • (旧文)在 micropython / esp-at / arduino 中实现 软串口(software-serial) 的参考

    本文是迁移一年半前我在 https://github.com/junhuanchen/esp-idf-software-serial 项目下写下的记录。

    ESP-IDF SoftWare Serial

    基于该项目 Github Arduino Esp32-SoftwareSerial

    花了点时间写了一下软串口,因为娱乐和工程需要,所以我从过去自己在 Arduino 上实现的软串口移植到 ESP-IDF 下,为此也写一周了吧,使用硬件为 Bpi:Uno (esp32)。

    更新了一次 esp8266 rtos 用的软串口,大概只做到了 57600 这个范围内稳定使用,但开头总有一两个字节要出错,应该是硬件电平上的干扰,持续使用是没有问题的。
    soft_urat_esp8285_57600.c

    本模块的意义是?

    大多数国产/传统的传感器接口,会采用 9600 的通信协议,而 ESP32/ESP8266 的硬串口很少(其中一个无法进行发送数据),举例来说,如果我们想要集成了 GRPS 模块、RS232 模块、PMX.X 模块、MicroPython REPL、XX 串口传感器的模块,此时怎样都不够用,所以软串口可以解决此问题。

    注意不能使用的引脚

    在 arduino 里有这样的定义,NULL 的意味着它无法作为接收引脚,其他的一般都可以作为发送引脚,注意别和硬串口冲突(比如 0 2 16 17 ),不然就是浪费了。

    
    static void (*ISRList[MAX_PIN + 1])() = {
        sws_isr_0,
        NULL,
        sws_isr_2,
        NULL,
        sws_isr_4,
        sws_isr_5,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        sws_isr_12,
        sws_isr_13,
        sws_isr_14,
        sws_isr_15,
        sws_isr_16,
        sws_isr_17,
        sws_isr_18,
        sws_isr_19,
        NULL,
        sws_isr_21,
        sws_isr_22,
        sws_isr_23,
        NULL,
        sws_isr_25,
        sws_isr_26,
        sws_isr_27,
        NULL,
        NULL,
        NULL,
        NULL,
        sws_isr_32,
        sws_isr_33,
        sws_isr_34,
        sws_isr_35};
    
    

    ESP32 Unit Test

    • 9600 在 ESP-IDF 和 MicroPython 环境下测试完美,其中 0x00 - 0xFF 256 个字节的数据发送与接收均正常,多次测试的结果非常好。
    • 57600 在 ESP-IDF 环境下测试和 9600 的效果一致,但是 MicroPython 中多次发送数据后,数据会抖动,需要优化一下每个字节的发送部分的间隔才能改善,另外 0x00 - 0xFF 256 个字节的数据接收正常。
    • 115200 在 ESP-IDF 环境下测试收发通信正常,但是在 MicroPython 下无法正常,轻微的 4us 误差数据抖动,就会导致每次采集数据不准确,也没有对此添加过采样(多次采样选其一),所以需要设定波特率到 136000 才能相对准确(更快的发送,从而忽略掉两次执行发送间隔的影响,这个部分我想在还需要多加优化才能相对完美)。

    所以 esp-idf 中,你可以任意使用 9600 57600 115200 的波特率,但如果发现存在问题,需要去修改 sw_serial.h 中的两个参数 rx_start_time 和 rx_end_time ,或设置其他波特率。

    
    // suggest max datalen <= 256 and baudRate <= 115200
    esp_err_t sw_open(SwSerial *self, uint32_t baudRate)
    {
        // The oscilloscope told me
        self->bitTime = (esp_clk_cpu_freq() / baudRate);
    
        // Rx bit Timing Settings
        switch (baudRate)
        {
            case 115200:
                self->rx_start_time = (self->bitTime / 256);
                self->rx_end_time = (self->bitTime * 127 / 256);
                break;
            
            case 9600:
                self->rx_start_time = (self->bitTime / 9);
                self->rx_end_time = (self->bitTime * 8 / 9);
                break;
            
            default: // tested 57600 len 256
                self->rx_start_time = (self->bitTime / 9);
                self->rx_end_time = (self->bitTime * 8 / 9);
                break;
        }
        
        // printf("sw_open %u %d
    ", self->rx_start_time, self->rx_end_time);
    
        sw_write(self, 0x00); // Initialization uart link
    
        return sw_enableRx(self, true);
    }
    
    

    而 micropython 中,不超过 57600 都是可以正常使用的,但 115200 只能靠改参数来满足,比如 115200 改成 137000 可以让局部数据准确传输,通常我们认为完整的数据范围是 0x00 - 0xFF 之间。

    DSView Tool

    对于其中的数据传输情况,你需要一个逻辑分析仪,例如我使用的是这个 dreamsourcelab

    比如我下图做的分析。

    MicroPyhton 的效果

    harvest

    首先我学会了使用逻辑分析仪 :),我可以自己去捕获数据的情况来分析数据源,分析它的发送和接收都是相对麻烦的事情,但从编程上讲,一定是发送要比接收更简单。

    how to do

    首先我最早在 Arduino 上使用软串口,作为软件出身的,我知道如何进行逻辑分析和拆解,因此我从 Arduino 上分离了逻辑到 ESP-IDF ,但是当我移植完成后完全不能使用,因为这个不同于软件模块,迁移之后只需要关系逻辑问题,这个还要结合通信时序来分析问题。

    因此,我移植完成后,先审核数据发送接口的逻辑,最先遇到的问题的是 主频 和 时间周期的关系,经过群里小伙伴的教育后,我才知道 波特率 以及主频频率的意义,所谓的 115200 波特率是指 1 * 1000 * 1000 us 下的 bit 数量(可能描述不准确,主要就是量化 bit 的传输周期),因此在 1 秒下 115200 波特率的 bit 周期为 8 us , 结合标准的传输内容 起始位 数据位 停止位,总共 10 个 bit ,也就是 80 us 一个字节,因此 115200 下传输一个字节需要 80 us 左右。

    基于此继续说,所以发送的时候,假设为上升沿触发后,将会持续 80us 的过程进行字节判定,对方将会捕获此数据进行协议解析,而没有逻辑分析仪,你就无法准确判断,怎么才是正确的数据。

    所以我从师弟手上抢掠了一台萝莉分析仪,先是捕获 CH340 的发送数据,以确保标准发送的数据源,再结合自己产生的数据做比较,结果才发现,发送的逻辑结构是一样的,但周期间隔完全不一样,因此假设逻辑已经正确,消除时序的周期差异,只需要解决差异的倍数就可以了,所以回到 主频 和 时间的关系,例如 esp32 160 的时候此时 芯片的 1 us 差应该如何获得,为了能够创造这个 1 us 的关系,实际上就是假设为单周期的计数器的结果,所以我们可以假设 160 次累加后相当于 1 us,所以 8u 就是放大 8 * 160 的结果,有了这个基准,就可以准确的进行每次数据的发送间隔,在代码中的 WaitBitTime 和 getCycleCount 就是做这个用途的。

    有了周期的基准,也有了逻辑结构,程序的功能已经成型,那么就是核对测试的问题。

    本以为有了这一切都已经可以正常发送数据了后。

    结果发现硬件存在一点差异化问题,不知为何,第一个字节发送的一定会错误,因为分析仪得到的数据中有一个极小的抖动,突然向下跳了一下,导致后续数据混乱,所以先发一个数据,清理掉这个不知道是不是上电带来的影响(软解),此后数据一切正常。

    接着测试发送 0x00 - 0xFF 的定义域数据,核对边界,切换 9600 、57600、115200 进行核对,期间没有什么异常。

    进入到接收部分开发,中断触发已经确认,但发现,此时逻辑分析仪已经无法派上用场了,因为解析全指望芯片的寄存器和中断函数不出问题,处理这部分的时候,幸运的结合了 Arduino 的经验,假设数据源为 CH340 ,选择起始位的上升沿的 1 / 9 区域作为捕获上升沿信号的采样(没有过采样),此后依次采样,然后 停止位的时候 8 / 9 的区域收尾停止位 bit ,此时一帧完成。

    这个逻辑在 9600 和 57600 的时候没有出现太大问题,当 115200 出现后,1 / 9 的比例无法保障 255 字节数据传输过程中的执行误差。(还是因为没有过采样XD),所以 115200 的时候,出现了接收错误,没有办法使用逻辑分析仪,结合代码逻辑,尽量优化中断函数的操作,然后确保每次中断函数的独占和退出都最小化影响,并调整到 1 / 255 的区间,此时 255 定义域字节数据一切正常,测试完成。

    ESP-IDF 开发完成,移植到 MicroPyton 存在的问题。

    主要是发送数据的函数间隔和接收数据的其他函数影响,总体来讲。

    发送函数在 Python 环境中,所以 115200 的时候,数据位的发送过程中与 标准源的误差去到了 4us ,这意味着可能错过半个位,因此可以通过设置较高的波特率调快发送的位等待(bit wait time),但接收函数就无法保证了,所以 115200 还存在一些需要深度优化的才能解决的细节问题(比如过采样XD,也需要测试一下 ESP32 的 IO 翻转速度)。

    problem

    目前是半双工的软串口,所以你需要一个 CH340 之类的做数据收发测试,注意发送的数据,不能乱发,容易让电脑蓝屏(使用的时候尽量是 ASCII)。

    MicroPython 暂时无法使用 115200 的波特率,但你如果是指定的某些数据协议,还是可以通过修改源码的时序尽可能解决的,但这个做法并不通用。

    还需要更长的数据和更大量的数据传输来测试,否则也只是消费级的娱乐代码水准。

    result

    最后 萝莉分析仪真是个好东西,来一张全家桶合照。

    2020年10月5日 更新内容

    由于上述内容是过去的旧文,所以我一般不会按后来的观念去重新整理了。

    我最近更新了一次 esp8285 在 esp-at rtos 下实现 57600 波特率的串口传输,严格来讲,对以前的一些问题有不一样的认识了,得益于这一年对硬件特性的理解加深了许多。

    例如为什么第一个字节会出错,例如如何进行自适应数据,例如如何过采样,总得来说,现在的实现手段比过去要更加精准的获取想要的数据了,也更加的清楚硬串口怎样做才可以高速率高精度的传输了。

    为什么第一个字节会出错

    先来问答第一个问题,为什么第一个字节会错,这是因为 gpio 的通道打开后,电路中存在电容、电阻等元件,会影响 GPIO 的初次电平瞬间不稳定,其实这样去思考问题就知道为什么了。

    如何改进芯片接收端逻辑

    这次补全另外两个接收逻辑的代码,可以不用像以往那样要求严格的读取,但要注意每段 gpio 改变电平的语句执行周期可能会到 1us 所以在制作 esp8285 的 115200 时异常难以克服这种误差,因为 GPIO 的硬件资源工作需要时间,115200 要求 10us 内完成对一个位的判定,也就是延时精度必须控制在 1us 以下才可以精准的采集相应的信号,到了这一层基本上都看不到了,如果不配合硬件测量,软件只能凭感觉来写。

    软串口芯片的接收逻辑我放一张图做说明。

    我们看图可知,实际上只需要芯片在接收的时候能够在 起始位 到 停止位 之间的 8 个 bit 位被采集出来,但会出现几个问题。

    • 我们一般情况下无法捕捉此时芯片接收期间的所触发的信号位,间隔太小了,如 115200 在 10us 内准确的确认输入的电平位。
    • 控制 GPIO 的翻转速度并非能够一直保持稳定的工作状态,必然会存在抖动和硬件误差。
    • 控制 GPIO 电平 由高到低 与 由低到高 并不像字面上看到的等效执行周期,嗯,也许字面上上看起来是一样的,但实际的执行结果并不一样。

    这些误差就让通常简单的软件逻辑很难在长时间的运行过程中保持绝对不出错,必须将多次字节可能导致的误差纳入接收逻辑的考虑范畴。

    因此我们必须把所有语句和硬件可能的影响带入考虑,这之后我引入了两段函数的逻辑来克服真实世界中存在误差的问题。

    https://github.com/junhuanchen/esp-idf-software-serial/blob/70e335880babbc036f79639ccf6247029d850dee/soft_urat_esp8285_57600.h#L108-L132

    在这之前,接收逻辑是以一个字节为单位的,所以起始位的判断会影响到后续的数据和停止位的判断,一般我们以触发沿的三分之一部分为采样点,这就导致了后续的采样点均保持同一个间隔进行。

    所以我现在改成,等待某个 起始位 或 停止位 输入的方式进行数据的触发,如下代码。

    
    static inline bool IRAM_ATTR wait_bit_state(uint8_t pin, uint8_t state, uint8_t limit)
    {
        for (uint i = 0; i != limit; i++)
        {
            // ets_delay_us(1);
            WaitBitTime(limit);
            if (state == gpio_get_level(pin))
            {
                return true;
            }
        }
        // ets_delay_us(1);
        return false;
    }
    
    

    调用 if (wait_bit_state(self->rxPin, self->invert, self->rx_start_time)) 它的效果是期望 在指定的 limit 时间内 捕获到 期望的 invert 状态值 后 返回 真 。

    这样,如果没有在期望的周期里得到该数据,则说明不存在数据,或者数据溢出,从而灵活的避开了起始位的判断,与 停止位的固定等待触发结束。

    • 起始位的触发不再通过固定的跳变周期触发,从而取消了起始位触发的区间设置。
    • 停止位也可以尽快结束,尽快离开本次中断,等待下一次的起始位的触发。

    接着是提供一种过采样的方式,以往我们可以使用多次采样选择一次准确的值作为真实值。

    
    static inline uint8_t IRAM_ATTR check_bit_state(uint8_t pin, uint8_t limit)
    {
        uint8_t flag[2] = { 0 };
        for (uint i = 0; i != limit; i++)
        {
            flag[gpio_get_level(pin)] += 1;
            ets_delay_us(1);
        }
        return flag[0] < flag[1];// flag[0] < flag[1] ? 1 : 0;
    }
    
    

    这个函数的思路就是,准备一个 0 和 1 的缓冲区,进行读取后直接对索引的缓冲区做加法,最后通过比较大小来返回最大的可能性,表示真正采样的电平值最多的作为最终理想的结果给回。

    不过,这样的过采样也存在一些风险,在 esp8285 中低于 10us 的采样中,要把函数执行周期(如 gpio_get_level)带来的误差也要一并考虑,所以在使用的时候要注意过采样函数的执行时间应小于理论上的时间。

    但在这里还会存在新的问题,例如大量连续发送数据后,每个字节的总体数据位的传输周期也并非固定且稳定的,如果考虑到真实情况下动态长度的数据接收,目前的解决方法就是通过协议来检测是否存在缓冲区溢出来消除这种缺陷。

    还有很多问题要克服,总之,目前通过这两个新加的函数,就可以把过去的期望一次周期采样数据准确的逻辑给强化了,希望在之后的使用中,我会进一步的把问题彻底解决吧,思路就先到这里了。

    后记

    截至目前,我还未将 115200 的稳定工作在 esp8285 下(可用,但未能通过我的要求&标准),仅是将 57600 20us 的 0x00 ~ 0xFF 在基于 rtos 的 esp-at 下做到稳定通信了,之后有必要我会继续做的。

    除了上面的函数方法,其实还有一些可以微调的参数,就比如 esp8266 的设置 115200 会在硬件传参的时候相对的提高一些,让每个位的检测降低到 8~9us 一个位附近,来克服整体的通信上存在的硬件误差,当然通过硬件来实现的必然要精准的多,不需要通过语句来交互判断,自然就不存在执行 gpio 函数过程中带来的误差。

    现在多结合一些硬件的状态来考虑问题,就可以写出比较健壮的代码了。

    2020年10月6日 junhuanchen

  • 相关阅读:
    Java Swing 空布局
    Java Swing 盒布局管理器
    遇到的JDBC的一些简单错误
    JVM内存调整
    jeesite中重启项目时用户头像丢失的疑惑
    Java服务突然失败:A fatal error has been detected by the Java Runtime Environment的总结
    从官网下载历史版本的java
    容器未来会怎么发展?容器安全怎么样?容器使用效果如何?
    What's The Next|Kube-OVN 社区线上 Meetup 预告!
    灵雀云发布云原生制品仓库Harbor企业版(Alauda Registry Service for Harbor)
  • 原文地址:https://www.cnblogs.com/juwan/p/13772185.html
Copyright © 2011-2022 走看看