YModem 串口测试软件

最近研究 YModem 协议需要有对应的软件进行测试,开始使用的是 WinXP 的超级终端,但是发现它无法支持超过 1Mhz 的频率,之后使用 ExtraPutty 发现它在无法工作在 2Mhz 的情况下。最终找到了一个开源的 YModem 串口测试软件:SerialPortYmodem,这是一个开源的工具在:

https://github.com/1021256354/SerialPortYmodem

它提供了源代码,但是没有提供 Windows 的可执行程序,只好手工编译,编译过程又发现它使用的是 QT 5.7 ,于是又研究如何安全QT 最终得到的编译结果。有同样需求的朋友可以在这里下载到编译结果。编译出来的可执行程序真不大160K 左右,但是如果想让这个程序在Windows下执行起来需要 48MB 的DLL 。

我在源代码的基础上还做了如下修改:

1.将中文提示信息替换为英文;

2.增加了一些波特率,比如:1,500,000和 2,000,000

3.增加了速度的显示,在 Send 和 Receive 之间可以看到当前速度

2,000,000 波特率通讯测试

编译好的Windows 可执行文件在下面(QT 框架编译出来的程序160K, 支持这个文件的 DLL 32MB):

链接: https://pan.baidu.com/s/1qPJT_jFvkZW1LO02BFjOWA 提取码: b9vz 复

修改后的源代码下载

FireBeetle 播放音频的更大存储空间

前面的文章介绍了 DAC 方式播放和用更高精度的 PWM 方式直接播放音频文件,但是很明显我们遇到了的2个问题:

  1. 存储空间有限,APP 中最大只能存放2.7MB的音频;
  2. 每次都需要手工将数据转化为 .h ,比较麻烦。

这次就介绍如何在 FireBeetle上使用更大的空间。从介绍中可以看到FireBeetle Flash 为16MB,但是 APP 最大只能用到3MB,余下的空间要么分配给 SPIFFS,要么分给 FATFS。

SPIFFS 和 FATFS 都是一种文件系统。其中SPIFFS 是一个用于 SPI NOR flash 设备的嵌入式文件系统,支持磨损均衡、文件系统一致性检查等功能【参考1】。同样的 FATFS也是一种文件系统【参考2】。很明显 ESP32 上我们可以使用更大的空间,因此这里尝试使用 FATSFS来存储音频文件。

对于 ESP32 来说,每次上传的 APP 和 FATFS 是分开的。比如,编译之后生成了一个 3MB 的APP, 那么上传时只会烧写更新 3MB APP那一段的SPI NOR中的内容,余下的部分不会改变(加上压缩以及快速的串口通讯,我们并不会感觉上传太慢)。因此,我们还需要一个额外的工具来完成上传 FATFS 的部分。这个工具就是arduino esp32fs 插件,这个插件能将文件传输到ESP32 的SPIFFS, LittleFS 或者FatFS分区上,项目地址如下:

https://github.com/lorol/arduino-esp32fs-plugin

下载到编译好的文件是一个 JAR文件。

安装方法是在你 Arduino.exe 的目录中,Tools 下创建 ESP32FS/Tool 目录,然后将上面这个文件放置进去:

重启 Arduino 之后在 Tools 菜单中会出现 “ESP32 Sketch Data Upload”的选项。

为了给 FATFS 分区上传,我们还需要2个额外的工具 mklittlefs.exe 和 mkfatfs.exe。这两个工具需要放在C:\Users\用户名\AppData\Local\Arduino15\packages\firebeetle32\hardware\esp32\0.1.1\tools 目录下。

上面准备妥当之后,先编写一个测试程序。这个程序会列出当前 FATFS 上面的文件名称。

#include "FS.h"
#include "FFat.h"

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\r\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("- failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println(" - not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if(levels){
                listDir(fs, file.name(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("\tSIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }

    
}

void setup() {
  Serial.begin(115200);
  if(!FFat.begin()){
        Serial.println("FFat Mount Failed");
        return;
  }  
    Serial.printf("Total space: %10u\n", FFat.totalBytes());
    Serial.printf("Free space: %10u\n", FFat.freeBytes());  
    
}

void loop() {
    listDir(FFat, "/", 0);
    delay(5000);
}

此外,我们还要在程序目录下创建一个 data 目录,然后将3支歌曲的文件放在里面。

直接使用菜单上传FATFS分区。

选择 FATFS:

看到下面的字样就表示已经成功:

接下来,像普通 Arduino 代码一样上传我们的程序,打开串口就能看到结果:

有了上面的代码,我们可以很容易编写出来播放代码:

#include "FS.h"
#include "FFat.h"

#include "data\1990.h"
#include "soc/sens_reg.h"  // For dacWrite() patch, TEB Sep-16-2019

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\r\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("- failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println(" - not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if(levels){
                listDir(fs, file.name(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("\tSIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }

    
}

void playFile(fs::FS &fs, const char * path){
    Serial.printf("Playing file: %s\r\n", path);

    File file = fs.open(path);
    if(!file){
        Serial.println("- failed to open file for reading");
        return;
    }

    uint8_t buffer[1024*4];
    size_t bytessend;
    while(file.available()){
        bytessend=file.read(buffer, 1024*4);
        for (int i=0;i<1024*4;i++) {
            CLEAR_PERI_REG_MASK(SENS_SAR_DAC_CTRL2_REG, SENS_DAC_CW_EN1_M);
            SET_PERI_REG_BITS(RTC_IO_PAD_DAC1_REG, RTC_IO_PDAC1_DAC, buffer[i], RTC_IO_PDAC1_DAC_S);
            SET_PERI_REG_MASK(RTC_IO_PAD_DAC1_REG, RTC_IO_PDAC1_XPD_DAC | RTC_IO_PDAC1_DAC_XPD_FORCE);
            ets_delay_us(125);
        }
    }
    file.close();
}
void setup() {
  Serial.begin(115200);
  if(!FFat.begin()){
        Serial.println("FFat Mount Failed");
        return;
  }  
    Serial.printf("Total space: %10u\n", FFat.totalBytes());
    Serial.printf("Free space: %10u\n", FFat.freeBytes());  
    
    listDir(FFat, "/", 0);
}

void loop() {
    playFile(FFat, "/1st.wav");
    playFile(FFat, "/10years.wav");
    playFile(FFat, "/1990.wav");
}

参考:

  1. https://docs.espressif.com/projects/esp-idf/zh_CN/release-v4.1/api-reference/storage/spiffs.html
  2. https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/storage/fatfs.html

VS2015 生成后事件

有些情况下,我们希望在编译完成后针对生成的 EXE 多做一些事情,比如:重命名,加入校验功能等等。为此,需要使用 VS 的“生成后事件”功能。

举例如下:

1.在你的 VC 项目上,点击右键打开属性

2. “生成后事件”位置如下图所示。特别注意:在上方的 Configuration 中需要选择这个动作的条件。比如,你设置了 X64 Release的动作,但是如果当前是 X64 Debug,那么编译过程中不会执行该动作。

3. 这里我们使用一个 dir 和 echo 命令(使用“&” 可以连接2个DOS 命令)

4.运行结果,可以看到编译结束后运行了上面的2个命令

参考:

  1. https://developer.aliyun.com/article/260394 VS中的预先生成事件和后期生成事件
  2. https://www.cnblogs.com/panmy/p/5831146.html VS中的生成事件
  3. https://blog.csdn.net/face_to/article/details/99968278 VS2015编译生成后事件处理

ESP32 YModem 的测试例子

根据 https://github.com/loboris/ESP32_ymodem_example 修改的 Arduino 版本的 ESP32 代码,在 FireBeelte 上测试通过。通过USB 串口进行测试,接收到的数据并不会存放在任何地方。

//https://github.com/loboris/ESP32_ymodem_example/blob/master/components/ymodem/ymodem.c
#include "ymodem.h"

//------------------------------------------------------------------------
static unsigned short crc16(const unsigned char *buf, unsigned long count)
{
  unsigned short crc = 0;
  int i;

  while(count--) {
    crc = crc ^ *buf++ << 8;

    for (i=0; i<8; i++) {
      if (crc & 0x8000) crc = crc << 1 ^ 0x1021;
      else crc = crc << 1;
    }
  }
  return crc;
}

//--------------------------------------------------------------
static int32_t Receive_Byte (unsigned char *c, uint32_t timeout)
{
    unsigned char ch;
    //ZivDebug int len = uart_read_bytes(EX_UART_NUM, &ch, 1, timeout / portTICK_RATE_MS);
    //ZivDebug_Start
    int len=0;
    unsigned int Elsp=millis();
    while ((millis()-Elsp<timeout / portTICK_RATE_MS)&&(len==0)) {
            if (Serial.available()) {
                    ch=Serial.read();
                    len=1;    
                    #ifdef ENDEBUG
                      Serial2.print("ESP32 RCV1:");
                      Serial2.print(ch,HEX);
                      Serial2.println(" ");                    
                    #endif
            }
    }
    //ZivDebug_End
    if (len <= 0) return -1;

    *c = ch;
    return 0;
}

//------------------------
static void uart_consume()
{
  uint8_t ch[64];
    //ZivDebug while (uart_read_bytes(EX_UART_NUM, ch, 64, 100 / portTICK_RATE_MS) > 0) ;
    //ZivDebug_Start
    int len=0;
    unsigned int Elsp=millis();
    while ((millis()-Elsp<100 / portTICK_RATE_MS)||(len<64)) {
            if (Serial.available()) {

                    ch[len]=Serial.read();
                    #ifdef ENDEBUG
                        Serial2.print("ESP32 RCV2:");
                        Serial2.print(ch[len],HEX);
                        Serial2.println(" ");
                    #endif  
                    len++;
            }
    }
    //ZivDebug_End
}

//--------------------------------
static uint32_t Send_Byte (char c)
{
  //ZivDebug uart_write_bytes(EX_UART_NUM, &c, 1);
#ifdef ENDEBUG
        Serial2.print("ESP32 send:");
        Serial2.print(c,HEX);
        Serial2.println(" ");
#endif          
  Serial.write(c); //ZivDebug
  return 0;
}

//----------------------------
static void send_CA ( void ) {
  Send_Byte(CA);
  Send_Byte(CA);
}

//-----------------------------
static void send_ACK ( void ) {
  Send_Byte(ACK);
}

//----------------------------------
static void send_ACKCRC16 ( void ) {
  Send_Byte(ACK);
  Send_Byte(CRC16);
}

//-----------------------------
static void send_NAK ( void ) {
  Send_Byte(NAK);
}

//-------------------------------
static void send_CRC16 ( void ) {
  //Serial2.print("SNDCRC16");
  Send_Byte(CRC16);
}


/**
  * @brief  Receive a packet from sender
  * @param  data
  * @param  timeout
  * @param  length
  *    >0: packet length
  *     0: end of transmission
  *    -1: abort by sender
  *    -2: error or crc error
  * @retval 0: normally return
  *        -1: timeout
  *        -2: abort by user
  */
//--------------------------------------------------------------------------
static int32_t Receive_Packet (uint8_t *data, int *length, uint32_t timeout)
{
  int count, packet_size, i;
  unsigned char ch;
  *length = 0;
  //Serial2.print("Receive_Packet:");
  // receive 1st byte
  if (Receive_Byte(&ch, timeout) < 0) {
    return -1;
  }
  //Serial2.print("Rcv5:");
  //Serial2.println(ch,HEX);
  switch (ch) {
    case SOH:
    packet_size = PACKET_SIZE;
    break;
    case STX:
    packet_size = PACKET_1K_SIZE;
    break;
    case EOT:
        *length = 0;
        return 0;
    case CA:
      //Serial2.print("CA:");
      if (Receive_Byte(&ch, timeout) < 0) {
        return -2;
      }
      if (ch == CA) {
        *length = -1;
        return 0;
      }
      else return -1;
    case ABORT1:
    case ABORT2:
      return -2;
    default:
      vTaskDelay(100 / portTICK_RATE_MS);
      uart_consume();
      return -1;
  }

  *data = (uint8_t)ch;
  uint8_t *dptr = data+1;
  count = packet_size + PACKET_OVERHEAD-1;
  //Serial2.print("Rcv3:");
  //Serial2.println(count);
  for (i=0; i<count; i++) {
    if (Receive_Byte(&ch, timeout) < 0) {
      return -1;
    }
    *dptr++ = (uint8_t)ch;;
  }
  //Serial2.print("Rcv4:");
  //Serial2.println(i);
  if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff)) {
      *length = -2;
      return 0;
  }
  if (crc16(&data[PACKET_HEADER], packet_size + PACKET_TRAILER) != 0) {
      *length = -2;
      return 0;
  }

  *length = packet_size;
  //Serial2.print("Rcv2:");
  //Serial2.println(packet_size);
  return 0;
}

// Receive a file using the ymodem protocol.
//-----------------------------------------------------------------
int Ymodem_Receive (FILE *ffd, unsigned int maxsize, char* getname)
{
  uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD];
  uint8_t *file_ptr;
  char file_size[128];
  unsigned int i, file_len, write_len, session_done, file_done, packets_received, errors, size = 0;
  int packet_length = 0;
  file_len = 0;
  int eof_cnt = 0;
  
  for (session_done = 0, errors = 0; ;) {
    for (packets_received = 0, file_done = 0; ;) {
      //LED_toggle();
      switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT)) {
        case 0:  // normal return
          switch (packet_length) {
            case -1:
                // Abort by sender
                send_ACK();
                size = -1;
                goto exit;
            case -2:
                // error
                errors ++;
                if (errors > 5) {
                  send_CA();
                  size = -2;
                  goto exit;
                }
                send_NAK();
                break;
            case 0:
                // End of transmission
              eof_cnt++;
              if (eof_cnt == 1) {
                send_NAK();
              }
              else {
                send_ACKCRC16();
              }
                break;
            default:
              // ** Normal packet **
              if (eof_cnt > 1) {
              send_ACK();
              }
              else if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0x000000ff)) {
                errors ++;
                if (errors > 5) {
                  send_CA();
                  size = -3;
                  goto exit;
                }
                send_NAK();
              }
              else {
                if (packets_received == 0) {
                  // ** First packet, Filename packet **
                  if (packet_data[PACKET_HEADER] != 0) {
                    errors = 0;
                    // ** Filename packet has valid data
                    if (getname) {
                      for (i = 0, file_ptr = packet_data + PACKET_HEADER; ((*file_ptr != 0) && (i < 64));) {
                        *getname = *file_ptr++;
                        getname++;
                      }
                      *getname = '\0';
                    }
                    for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < packet_length);) {
                      file_ptr++;
                    }
                    for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH);) {
                      file_size[i++] = *file_ptr++;
                    }
                    file_size[i++] = '\0';
                    if (strlen(file_size) > 0) size = strtol(file_size, NULL, 10);
                    else size = 0;

                    // Test the size of the file
                    if ((size < 1) || (size > maxsize)) {
                      // End session
                      send_CA();
                      if (size > maxsize) size = -9;
                      else size = -4;
                      goto exit;
                    }

                    file_len = 0;
                    send_ACKCRC16();
                  }
                  // Filename packet is empty, end session
                  else {
                      errors ++;
                      if (errors > 5) {
                        send_CA();
                        size = -5;
                        goto exit;
                      }
                      send_NAK();
                  }
                }
                else {
                  // ** Data packet **
                  // Write received data to file
                  if (file_len < size) {
                    file_len += packet_length;  // total bytes received
                    if (file_len > size) {
                      write_len = packet_length - (file_len - size);
                      file_len = size;
                    }
                    else write_len = packet_length;

                    //ZivDebug int written_bytes = fwrite((char*)(packet_data + PACKET_HEADER), 1, write_len, ffd);
                    int written_bytes=write_len;
                    if (written_bytes != write_len) { //failed
                      /* End session */
                      send_CA();
                      size = -6;
                      goto exit;
                    }
                    //LED_toggle();
                  }
                  //success
                  errors = 0;
                  send_ACK();
                }
                packets_received++;
              }
          }
          break;
        case -2:  // user abort
          send_CA();
          size = -7;
          goto exit;
        default: // timeout
          if (eof_cnt > 1) {
          file_done = 1;
          }
          else {
        errors ++;
        if (errors > MAX_ERRORS) {
        send_CA();
        size = -8;
        goto exit;
        }
        send_CRC16();
          }
      }
      if (file_done != 0) {
        session_done = 1;
        break;
      }
    }
    if (session_done != 0) break;
  }
exit:
  #if YMODEM_LED_ACT
  gpio_set_level(YMODEM_LED_ACT, YMODEM_LED_ACT_ON ^ 1);
  #endif
  return size;
}

//------------------------------------------------------------------------------------
static void Ymodem_PrepareIntialPacket(uint8_t *data, char *fileName, uint32_t length)
{
  uint16_t tempCRC;

  memset(data, 0, PACKET_SIZE + PACKET_HEADER);
  // Make first three packet
  data[0] = SOH;
  data[1] = 0x00;
  data[2] = 0xff;
  
  // add filename
  sprintf((char *)(data+PACKET_HEADER), "%s", fileName);

  //add file site
  sprintf((char *)(data + PACKET_HEADER + strlen((char *)(data+PACKET_HEADER)) + 1), "%d", length);
  data[PACKET_HEADER + strlen((char *)(data+PACKET_HEADER)) +
     1 + strlen((char *)(data + PACKET_HEADER + strlen((char *)(data+PACKET_HEADER)) + 1))] = ' ';
  
  // add crc
  tempCRC = crc16(&data[PACKET_HEADER], PACKET_SIZE);
  data[PACKET_SIZE + PACKET_HEADER] = tempCRC >> 8;
  data[PACKET_SIZE + PACKET_HEADER + 1] = tempCRC & 0xFF;
}

//-------------------------------------------------
static void Ymodem_PrepareLastPacket(uint8_t *data)
{
  uint16_t tempCRC;
  
  memset(data, 0, PACKET_SIZE + PACKET_HEADER);
  data[0] = SOH;
  data[1] = 0x00;
  data[2] = 0xff;
  tempCRC = crc16(&data[PACKET_HEADER], PACKET_SIZE);
  //tempCRC = crc16_le(0, &data[PACKET_HEADER], PACKET_SIZE);
  data[PACKET_SIZE + PACKET_HEADER] = tempCRC >> 8;
  data[PACKET_SIZE + PACKET_HEADER + 1] = tempCRC & 0xFF;
}

//-----------------------------------------------------------------------------------------
static void Ymodem_PreparePacket(uint8_t *data, uint8_t pktNo, uint32_t sizeBlk, FILE *ffd)
{
  uint16_t i, size;
  uint16_t tempCRC;
  
  data[0] = STX;
  data[1] = (pktNo & 0x000000ff);
  data[2] = (~(pktNo & 0x000000ff));

  size = sizeBlk < PACKET_1K_SIZE ? sizeBlk :PACKET_1K_SIZE;
  // Read block from file
  if (size > 0) {
    //ZivDebug size = fread(data + PACKET_HEADER, 1, size, ffd);
    //ZivDebug_Start
    for (i=0;i<size;i++){data[PACKET_HEADER+i]=i;}
    //ZivDebug_End
  }

  if ( size  < PACKET_1K_SIZE) {
    for (i = size + PACKET_HEADER; i < PACKET_1K_SIZE + PACKET_HEADER; i++) {
      data[i] = 0x00; // EOF (0x1A) or 0x00
    }
  }
  tempCRC = crc16(&data[PACKET_HEADER], PACKET_1K_SIZE);
  //tempCRC = crc16_le(0, &data[PACKET_HEADER], PACKET_1K_SIZE);
  data[PACKET_1K_SIZE + PACKET_HEADER] = tempCRC >> 8;
  data[PACKET_1K_SIZE + PACKET_HEADER + 1] = tempCRC & 0xFF;
}

//-------------------------------------------------------------
static uint8_t Ymodem_WaitResponse(uint8_t ackchr, uint8_t tmo)
{
  unsigned char receivedC;
  uint32_t errors = 0;

  do {
    if (Receive_Byte(&receivedC, NAK_TIMEOUT) == 0) {
      if (receivedC == ackchr) {
        return 1;
      }
      else if (receivedC == CA) {
        send_CA();
        return 2; // CA received, Sender abort
      }
      else if (receivedC == NAK) {
        return 3;
      }
      else {
        return 4;
      }
    }
    else {
      errors++;
    }
  }while (errors < tmo);
  return 0;
}


//------------------------------------------------------------------------
int Ymodem_Transmit (char* sendFileName, unsigned int sizeFile, FILE *ffd)
{
  uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD];
  uint16_t blkNumber;
  unsigned char receivedC;
  int i, err;
  uint32_t size = 0;

  // Wait for response from receiver
  err = 0;
  do {
    Send_Byte(CRC16);
    //LED_toggle();
  } while (Receive_Byte(&receivedC, NAK_TIMEOUT) < 0 && err++ < 45);

  if (err >= 45 || receivedC != CRC16) {
    send_CA();
    return -1;
  }
  
  // === Prepare first block and send it =======================================
  /* When the receiving program receives this block and successfully
   * opened the output file, it shall acknowledge this block with an ACK
   * character and then proceed with a normal YMODEM file transfer
   * beginning with a "C" or NAK tranmsitted by the receiver.
   */
  Ymodem_PrepareIntialPacket(packet_data, sendFileName, sizeFile);
  do 
  {
    // Send Packet
  //ZivDebug uart_write_bytes(EX_UART_NUM, (char *)packet_data, PACKET_SIZE + PACKET_OVERHEAD);
        //ZivDebug_Start
        //Serial2.print("ESP32 send:");
       // for (int i=0;i<PACKET_SIZE + PACKET_OVERHEAD;i++) {
       //         Serial2.print(packet_data[i],HEX);
       //         Serial2.print(" ");
       // }        
        for (int i=0;i<PACKET_SIZE + PACKET_OVERHEAD;i++) {
                Serial.write(packet_data[i]);
        }
        //ZivDebug_End
  // Wait for Ack
    err = Ymodem_WaitResponse(ACK, 10);
    if (err == 0 || err == 4) {
      send_CA();
      return -2;                  // timeout or wrong response
    }
    else if (err == 2) return 98; // abort
    //LED_toggle();
  }while (err != 1);

  // After initial block the receiver sends 'C' after ACK
  if (Ymodem_WaitResponse(CRC16, 10) != 1) {
    send_CA();
    return -3;
  }
  
  // === Send file blocks ======================================================
  size = sizeFile;
  blkNumber = 0x01;
  
  // Resend packet if NAK  for a count of 10 else end of communication
  while (size)
  {
    // Prepare and send next packet
    Ymodem_PreparePacket(packet_data, blkNumber, size, ffd);
    do
    {
        //uart_write_bytes(EX_UART_NUM, (char *)packet_data, PACKET_1K_SIZE + PACKET_OVERHEAD);
        //ZivDebug_Start
        //Serial2.print("ESP32 send:");
        //for (int i=0;i<PACKET_1K_SIZE + PACKET_OVERHEAD;i++) {
        //        Serial2.print(packet_data[i],HEX);
        //        Serial2.print(" ");
        //}
        //Serial2.println(" ");
        for (int i=0;i<PACKET_1K_SIZE + PACKET_OVERHEAD;i++) {
                Serial.write(packet_data[i]);
        }
        //ZivDebug_End

      // Wait for Ack
      err = Ymodem_WaitResponse(ACK, 10);
      if (err == 1) {
        blkNumber++;
        if (size > PACKET_1K_SIZE) size -= PACKET_1K_SIZE; // Next packet
        else size = 0; // Last packet sent
      }
      else if (err == 0 || err == 4) {
        send_CA();
        return -4;                  // timeout or wrong response
      }
      else if (err == 2) return -5; // abort
    }while(err != 1);
    //LED_toggle();
  }
  
  // === Send EOT ==============================================================
  Send_Byte(EOT); // Send (EOT)
  // Wait for Ack
  do 
  {
    // Wait for Ack
    err = Ymodem_WaitResponse(ACK, 10);
    if (err == 3) {   // NAK
      Send_Byte(EOT); // Send (EOT)
    }
    else if (err == 0 || err == 4) {
      send_CA();
      return -6;                  // timeout or wrong response
    }
    else if (err == 2) return -7; // abort
  }while (err != 1);
  
  // === Receiver requests next file, prepare and send last packet =============
  if (Ymodem_WaitResponse(CRC16, 10) != 1) {
    send_CA();
    return -8;
  }

  //LED_toggle();
  Ymodem_PrepareLastPacket(packet_data);
  do 
  {
  // Send Packet
  //ZivDebug uart_write_bytes(EX_UART_NUM, (char *)packet_data, PACKET_SIZE + PACKET_OVERHEAD);
        //ZivDebug_Start
        //Serial2.print("ESP32 send:");
        //for (int i=0;i<PACKET_SIZE + PACKET_OVERHEAD;i++) {
        //        Serial2.print(packet_data[i],HEX);
        //        Serial2.print(" ");
        //}
        //Serial2.println("");
        for (int i=0;i<PACKET_SIZE + PACKET_OVERHEAD;i++) {
                Serial.write(packet_data[i]);
        }
        //ZivDebug_End
  // Wait for Ack
    err = Ymodem_WaitResponse(ACK, 10);
    if (err == 0 || err == 4) {
      send_CA();
      return -9;                  // timeout or wrong response
    }
    else if (err == 2) return -10; // abort
  }while (err != 1);
  
  #if YMODEM_LED_ACT
  gpio_set_level(YMODEM_LED_ACT, YMODEM_LED_ACT_ON ^ 1);
  #endif
  return 0; // file transmitted successfully
}

void setup() {
  Serial.begin(921600);
  Serial2.begin(115200);
}

void loop() {
  char Filename[20];
  int sizesnd=Ymodem_Receive (NULL, 60*1024*1024, Filename);
  Serial2.print("Send bytes=");
  Serial2.println(sizesnd);
}

使用 Windows XP 的超级终端测试,在 921600 波特率情况下(超级终端支持的最高频率),传输速度可以达到 34KBytes/s。

超级终端传输

DFRobot 的 FireBeetle 上面使用的是 CH340C,最高可以支持 2,000,000的波特率。但是

Acpica 工具的重新编译方法

我们编译使用的 ACPI 工具iASL .exe通常来自Acpica。这个工具是开源的,本文将介绍如何在 Window 下编译。

首先,源代码可以来自https://github.com/acpica/acpica/releases 或者 https://acpica.org/downloads/windows-source。个人更推荐前者,后者在很多时候会有奇怪的问题。

接下来准备编译环境和工具。这次我使用 VS2019, 有兴趣的朋友可以使用这个 VS2019 离线安装包,安装方法很简单的,默认选项不需要联网即可完成安装。接下来需要安装3个工具:

1.GnuWin32  安装界面如下:

上面安装的是 GnuWin32 的安装包,上面的跑完了还要运行一下 install.bat. 特别注意,必须安装到 GnuWin32 目录下

2.接下来安装 Bison,特别注意需要安装到 Gnu32Win 目录下

3.安装 Flex,同样要特别安装到 GnuWin32 目录下。

4.上述安装好了之后需要将 c:\GnuWin32\bin 加入 Path 中。检查方法是设置之后,打开 CMD 窗口,输入 bison 和 flex,如果没有无法找到这个命令的错误,那就是正确的。

5.解压 acpica-R06_04_21.zip c:\apica 目录(必须是这个名字)。然后打开 generate\msvc2017目录下的 AslCompiler.dsw 文件。之后因为这个项目默认使用 VS2017,所以还要改动一下项目属性:

之后即可编译通过,比如我修改代码加入下面的字符串:

为了更加方便使用,这里提供了上面提到的工具。

  1. acpica-R06_04_21.zip 源代码
  2. GetGnuWin32-0.6.3.exe
  3. flex-2.5.4a-1.exe
  4. bison-2.4.1-setup.exe

如果不愿意进行安装,可以直接使用gnu4acpica这个压缩包,解压到 c:\GnuWin32 ,其中包括了 GnuWin32 Flex 和 Bison 无需额外安装即可编译通过。

链接: https://pan.baidu.com/s/1FcOOLI2OR-1tqwPLtYtuwg 提取码: r9ib

VC 显示设备管理器中有错误的设备

VS2015 编译通过,显示设备管理器中有错误的设备,代码如下:

// PrintDeviceInfo.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <Windows.h>
#include <setupapi.h>
#include <cfgmgr32.h>

#pragma comment(lib, "setupapi.lib")

int  _tmain(int  argc, _TCHAR* argv[])
{

	HDEVINFO hDevInfo;
	SP_DEVINFO_DATA DeviceInfoData;
	DWORD  i;

	// 得到所有设备 HDEVINFO      
	hDevInfo = SetupDiGetClassDevs(NULL, 0, 0, DIGCF_PRESENT | DIGCF_ALLCLASSES);

	if (hDevInfo == INVALID_HANDLE_VALUE)
		return  0;

	// 循环列举     
	DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
	for (i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &DeviceInfoData); i++)
	{
		char  szClassBuf[MAX_PATH] = { 0 };
		char  szDescBuf[MAX_PATH] = { 0 };
		DWORD dwDevStatus, dwProblem;

		// 获取类名  
		if (!SetupDiGetDeviceRegistryProperty(hDevInfo, &DeviceInfoData, SPDRP_CLASS, NULL, (PBYTE)szClassBuf, MAX_PATH - 1, NULL))
			continue;

		//获取设备描述信息
		if (!SetupDiGetDeviceRegistryProperty(hDevInfo, &DeviceInfoData, SPDRP_DEVICEDESC, NULL, (PBYTE)szDescBuf, MAX_PATH - 1, NULL))
			continue;

		CM_Get_DevNode_Status(&dwDevStatus, &dwProblem, DeviceInfoData.DevInst, 0);
		
		if (dwProblem != 0) {
			wprintf(L"Below device has a error:\r\n   Class:%s\r\n   Desc:%s\r\n\r\n", szClassBuf, szDescBuf);
		}

	}

	//  释放     
	SetupDiDestroyDeviceInfoList(hDevInfo);

	wprintf(L"Press anykey to exit.");
	getchar();

	return  0;
}

参考:

1.https://blog.csdn.net/flyingleo1981/article/details/53525060 获取设备管理器的信息 – VC

2.https://blog.csdn.net/d2262272d/article/details/105047066 判断usb硬件的驱动是否已安装

Step to UEFI (232)UEFI Shell 下控制 USBNotifier

前面的文章介绍了如何使用 CH55X 制作一个 USB 提醒器【参考1】,这次介绍如何在 UEFI Shell 下编写 Application 来控制使用它。

从思路上来说,可以使用加载驱动,然后调用驱动引入的 protocol 来进行控制。比如,FT232 有一个驱动,可以在 UEFI Shell 下直接调用【参考2】.但是,这次的USB 提醒器并没有这样的Driver(估计需要等待 WCH 来进行开发吧)。于是,我们只能尝试直接对其发送数据。

首先使用 USBView 查看一下:

USBViewer 查看结果

这里需要特别关注的是 Endpoint, 可以看到有下面三个 Endpoint, 第一个是 interrupt, 用于传送控制信息;后面两个分别是 Bulk 的输入和输出用于传输真正的串口数据。

          ===>Endpoint Descriptor&lt;===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x81  -> Direction: IN - EndpointID: 1
bmAttributes:                      0x03  -> Interrupt Transfer Type
wMaxPacketSize:                  0x0008 = 0x08 bytes
bInterval:                         0x40

          ===>Endpoint Descriptor&lt;===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x02  -> Direction: OUT - EndpointID: 2
bmAttributes:                      0x02  -> Bulk Transfer Type
wMaxPacketSize:                  0x0040 = 0x40 bytes
bInterval:                         0x00

          ===>Endpoint Descriptor&lt;===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x82  -> Direction: IN - EndpointID: 2
bmAttributes:                      0x02  -> Bulk Transfer Type
wMaxPacketSize:                  0x0040 = 0x40 bytes
bInterval:                         0x00

另外因为 USBNotifier 实际上是一个假串口设备(意思是并非对外转接为串口数据),所以即使初始化时如果没有设置波特率等等这样的参数,仍然能够正确收到数据。所以,我们只需要将数据丢到Bulk 的 Endpoint 就可以正常收到并处理。关键步骤如下:

  1. 枚举当前系统中有 USBIo 的全部 handle
  2. 使用 UsbGetDeviceDescriptor() 取得每个设备的 PID 和 VID
  3. 使用 UsbGetInterfaceDescriptor() 找到 USBNotifier 的 EndPoint
  4. 使用 UsbBulkTransfer() 针对找到的 Endpoint 发送数据

需要特别注意的是:在 UEFI Shell 下, USBNotifier 会被认为两个设备,Interrupt 的Endpoint 为一个设备,另外2个 Bulk 的 Endpoint 被识别为一个设备。

完整的代码:

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/ShellCEntryLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>
#include <Protocol/UsbIo.h>

extern EFI_BOOT_SERVICES         *gBS;
#define USB_ENDPOINT_ADDR(EpAddr) ((EpAddr) & 0x7F)

EFI_GUID gEfiUsbIoProtocolGuid =
{ 0x2B2F68D6, 0x0CD2, 0x44CF, { 0x8E, 0x8B, 0xBB, 0xA2, 0x0B, 0x1B, 0x5B, 0x75 }};

int
EFIAPI
main (
        IN int Argc,
        IN CHAR16 **Argv
)
{
        EFI_STATUS    Status;
        UINTN         HandleIndex, HandleCount;
        //UINT8         i,j;
        EFI_HANDLE    *DevicePathHandleBuffer = NULL;
        EFI_USB_IO_PROTOCOL          *USBIO;
        EFI_USB_DEVICE_DESCRIPTOR     DeviceDescriptor;
        EFI_USB_INTERFACE_DESCRIPTOR  IfDesc;
        UINT8                         arr[64];
        UINTN                         LengthInBytes;
        UINT32                        TransferStatus;

        //Get all the Handles that have UsbIO Protocol
        Status = gBS->LocateHandleBuffer(
                ByProtocol,
                &gEfiUsbIoProtocolGuid,
                NULL,
                &HandleCount,
                &DevicePathHandleBuffer);
        if (EFI_ERROR(Status))
        {
                Print(L"ERROR : Get USBIO count fail.\n");
                return 0;
        }

        for (HandleIndex = 0; HandleIndex < HandleCount; HandleIndex++)
        {
                Status = gBS->HandleProtocol(
                        DevicePathHandleBuffer[HandleIndex],
                        &gEfiUsbIoProtocolGuid,
                        (VOID**)&USBIO);
                if (EFI_ERROR(Status))
                {
                        Print(L"ERROR : Open USBIO fail.\n");
                        gBS->FreePool(DevicePathHandleBuffer);
                        return 0;
                }

                //Get USB Device Descriptor
                Status = USBIO->UsbGetDeviceDescriptor(USBIO, &DeviceDescriptor);
                if (EFI_ERROR(Status))
                {
                        Print(L"ERROR : Usb Get Device Descriptor fail.\n");
                        gBS->FreePool(DevicePathHandleBuffer);
                        return EFI_SUCCESS;
                }

                //Find the device which VID and PID is USB notifier
                if ((0x1209==DeviceDescriptor.IdVendor) && (0xC550==DeviceDescriptor.IdProduct))
                {
                        //Show the PID and VID
                        Print(L"Found a USB Notifier = %04X, ProductID = %04X\n",
                        DeviceDescriptor.IdVendor,
                        DeviceDescriptor.IdProduct);
                        //
                        // Get Interface Descriptor
                        //
                        Status = USBIO->UsbGetInterfaceDescriptor (USBIO, &IfDesc);
                        if (EFI_ERROR (Status))
                        {
                                continue;
                        }
                        
                        if (IfDesc.NumEndpoints==2)
                        {
                                Print(L"Found OUTPUT Endpoint, send the data!\n");
                                
                                arr[0]=0x5B; arr[1]=0x63; arr[2]=0xFF;
                                arr[3]=0x00; arr[4]=0x00; arr[5]=0x5D;
                                LengthInBytes=6;
                                
                                Status = USBIO->UsbBulkTransfer ( 
                                                        USBIO,
                                                        2,
                                                        &arr[0],
                                                        &LengthInBytes,
                                                        3000,
                                                        &TransferStatus );
                                Print(L"Red color\n");
                                gBS->Stall(3000000UL);
                                
                                arr[2]=00; arr[3]=0xFF; arr[4]=0x00;
                                Status = USBIO->UsbBulkTransfer ( 
                                                        USBIO,
                                                        2,
                                                        &arr[0],
                                                        &LengthInBytes,
                                                        3000,
                                                        &TransferStatus );
                                Print(L"Green color\n");
                                gBS->Stall(3000000UL);
                                
                                arr[2]=0x00; arr[3]=0x00; arr[4]=0xFF;
                                Status = USBIO->UsbBulkTransfer ( 
                                                        USBIO,
                                                        2,
                                                        &arr[0],
                                                        &LengthInBytes,
                                                        3000,
                                                        &TransferStatus ); 
                                Print(L"Blue color\n");
                                gBS->Stall(3000000UL);
                                
                                arr[2]=0x00; arr[3]=0x00; arr[4]=0x00;
                                Status = USBIO->UsbBulkTransfer ( 
                                                        USBIO,
                                                        2,
                                                        &arr[0],
                                                        &LengthInBytes,
                                                        3000,
                                                        &TransferStatus );   
                        }
                }
        }
        gBS->FreePool(DevicePathHandleBuffer);

        return EFI_SUCCESS;
}

工作的视频可以在 B 站看到。

https://www.bilibili.com/video/BV1dQ4y1X7XG/

完整工程下载:

参考:

  1. https://www.lab-z.com/usbnt/  做一个 USB 提醒器
  2. https://www.lab-z.com/stufdti/ Step to UEFI (93)FTDI 串口驱动

VC 获得系统的 MCFG Table

之前介绍过我们可以从内存来进行 PCI 配置空间的访问,前提是需要找到 PCI Base Address。在现在的系统中,这个地址是放在 ACPI MCFG Table 中来通知系统的。例如,下面就是我电脑上的 Base Address。

RW Everything 读取 MCFG Table

为了完成这个目标,分作两步,第一步是取得 MCFG Table,通过 GetSystemFirmwareTable() 函数来完成;第二步,根据 MCFG Structure Definitions 来解析 MCFG Table,即可获得我们需要的值。最终代码如下:

// GetMCFGTable.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include &lt;windows.h>
#include &lt;sysinfoapi.h>

//
// Common ACPI description table header.  This structure prefaces most ACPI tables.
//
#pragma pack(1)

typedef struct {
	UINT32  Signature;
	UINT32  Length;
	UINT8   Revision;
	UINT8   Checksum;
	UINT8   OemId[6];
	UINT64  OemTableId;
	UINT32  OemRevision;
	UINT32  CreatorId;
	UINT32  CreatorRevision;
} EFI_ACPI_DESCRIPTION_HEADER;

//
// MCFG Structure Definitions
//
//
// Memory Mapped Enhanced Configuration Base Address Allocation
// Structure Definition
//
typedef struct {
	UINT64  BaseAddress;
	UINT16  PciSegmentGroupNumber;
	UINT8   StartBusNumber;
	UINT8   EndBusNumber;
	UINT32  Reserved;
} EFI_ACPI_MEMORY_MAPPED_ENHANCED_CONFIGURATION_BASE_ADDRESS_STRUCTURE;

///
/// MCFG Table header definition.  The rest of the table 
/// must be defined in a platform specific manner.
///
typedef struct {
	EFI_ACPI_DESCRIPTION_HEADER                       Header;
	UINT64                                            Reserved;
} EFI_ACPI_MEMORY_MAPPED_CONFIGURATION_BASE_ADDRESS_TABLE_HEADER;


typedef struct {
	EFI_ACPI_MEMORY_MAPPED_CONFIGURATION_BASE_ADDRESS_TABLE_HEADER        Header;
	EFI_ACPI_MEMORY_MAPPED_ENHANCED_CONFIGURATION_BASE_ADDRESS_STRUCTURE  Segment;
} EFI_ACPI_MEMORY_MAPPED_CONFIGURATION_BASE_ADDRESS_TABLE;

#pragma pack()

EFI_ACPI_MEMORY_MAPPED_CONFIGURATION_BASE_ADDRESS_TABLE  MCFG;

int main()
{
	DWORD MCFGDataSize;

	MCFGDataSize=GetSystemFirmwareTable(
			'ACPI', 
			*(DWORD *)"MCFG", 
			&amp;MCFG,
			sizeof(EFI_ACPI_MEMORY_MAPPED_CONFIGURATION_BASE_ADDRESS_TABLE));

	printf("MCFG Table size = %d bytes\n", MCFGDataSize);
	
	printf("BaseAddress = 0x%X  \n", MCFG.Segment.BaseAddress);

	getchar();
    return 0;
}

运行结果:

国产高速芯片 CH9344 测试板

 CH9344是南京沁恒出品的一款串口芯片。现在市面上常见的 CH340 也是他们家的产品。相比之下,CH9344有下面2个显著的特点:

1.      最高支持 12,000,000串口频率;

2.      一颗芯片带有4个串口。

更具体的信息可以在官方页面【参考1】上看到。

这次我尝试使用这个芯片制作了一个测试板。首先是绘制电路图,基本上就是照搬 Datasheet上提供的参考设计。需要特别注意的是:

1.      因为通讯速度较高(已经属于 USB High Speed了),因此使用的是30Mhz 的晶振;

2.      Pin37 务必预留一个按钮,后面会讲述原因;

3.      USB 口上预留了一个 H3 跳线,用于一些不需要从 USB口取电的情况;

Layout 还是比较狂野的:

3D 预览如下:

然后就做出来了,焊接难度中等,强烈推荐准备助焊锡膏(不是焊锡膏),因为在焊接的时候很容易出现引脚粘连,在助焊锡膏的帮助下才容易分开。

完成之后就开始了测试。测试中我发现当选择为 12Mhz 通讯时,实际只有  6Mhz。

为了解决这个问题首先在技术社区发帖咨询,管理员让我通过电子邮件联系技术支持(tech@wch.cn)。技术支持工程是先后确认过使用的驱动是最新版本,通讯软件正确(他们家有自己的串口软件),之后他们发现我的芯片版本较老:

升级的方法是:对地短接 Pin37 ,将模块插入电脑后运行升级软件即可更新内部Firmware:

之后,设备管理器中的设备信息有变化:

再次测量,可以看到能够实现 12Mhz 的通讯:

电路设计软件是立创 EDA

参考:

1. http://www.wch.cn/products/CH9344.html

VC 宏展开

相信很多人入门时都使用 MASM,这的 MASM 就是 Microsoft‘s Macro Assembler。其中的 Macro 就是宏的意思。相比函数,宏具有更加简洁,运行速度快(编译器会对代码进行“宏展开”,直接修改代码)等等特点。但是,如果需要调试和阅读具有多层宏定义就非常痛苦了。很多年前我接触到的P公司的BIOS代码就是这样,乍一看代码非常规整,每一行就像一个洋葱,追踪起来一层又一层,让人感叹阅读代码是个系统工程。

最近偶然看到 GCC 有展开宏功能,同样的在Microsoft 的Visual C++上也有类似功能,通过编译指令 /p 或者 /ep 即可实现。这两个参数的区别在于生成的” 预编译文件”是否有行号。例如,编写下面的代码,其中定义一个名为 SUM 的宏:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#define SUM(a,b) a+b


/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
        int c =3,d=4;

  Print(L"Hello there fellow Programmer.\n");
  Print(L"Welcome to the world of EDK II.\n");

  Print(L"Macro test %d\n",SUM(c,d));
  
  return(0);
}

在 INF 中定义如下:

[BuildOptions]
  MSFT:*_*_X64_CC_FLAGS  = /P

在Build 目录下有生成一个 hello.i 其中有如下代码断,可以看到 SUM 宏已经展开。

INTN
__cdecl
ShellAppMain (
   UINTN Argc,
   CHAR16 **Argv
  )
{
        int c =3,d=4;

  Print(L"Hello there fellow Programmer.\n");
  Print(L"Welcome to the world of EDK II.\n");

  Print(L"Macro test %d\n",c+d);
  
  return(0);
}

如果使用 /EP  /P 参数,那么生成的预编译文件中不会有行号:

[BuildOptions]
  MSFT:*_*_X64_CC_FLAGS  = /EP /P

下图中,左侧是 /EP /P 参数的运行结果,右侧是/P 的结果: