硬盘清理神器:SpaceSniffer

随着科技的进步,现在的大多数电脑都在使用固态硬盘,但是因为价格的问题,你的办公电脑硬盘永远比实际需要小一个档次。经过一段时间硬盘空间就会变得捉襟见肘。为此,需要进行硬盘的清理,在这个过程中我不建议使用全自动工具清理,因为全自动工具通常删除的只是缓存内容,删除之后很可能影响性能,并且经过一段时间之后仍然会自动生成,另外,这种工具“深度清理”之后很可能导致系统奇怪的问题。我建议用户进行手工的清理,该删除的要删除,该备份的要即时备份。这里推荐名为 “SpaceSniffer” 的工具,能够帮助用户快速识别当前系统中占用硬盘最高的内容。

这个工具的官方网站如下:

http://www.uderzo.it/main_products/space_sniffer/index.html

软件是绿色并且免费的,无需安装:

解压之后直接运行  SpaceSniffer 即可。

首先会要求你选择扫描的路径,一般情况下我们会选择某个盘符,这里我们选择扫描 C盘。

扫描过程中可能会出现如下的提示信息,出现的原因是软件权限不够一些文件无法访问。我们可以忽略这个提示。如果不想看到这个的话,可以在运行时选择以管理员权限运行这个软件。

最终扫描结果如下:

整体是按照占用控件进行排序的,比如左上角是 Windows 目录占用了 25.3GB 的控件,其中WinSxS 占用了 11.3GB 的大小。鼠标移动到方块上之后可以直接打开目录进行详细的查看。

有兴趣的朋友不妨试试这款软件,帮助你节省更多空间出来。有兴趣的朋友请到上面给出的官网下载

FireBeetle 读取蓝牙键盘输入

取蓝牙键盘输入

Arduino 可以使用键盘作为输入设备,最常见的是下面2种接口的键盘:

  1. PS2 接口的键盘。缺点是这种接口键盘市面上非常少见
  • USB 键盘。这种键盘非常常见,为了在 Arduino 上使用,可以使用 USB Host Shield,缺点是占用SPI接口和多个GPIO;或者使用 CH9325 这种USB 转串口的芯片,这个方案的缺点是可能存在兼容性问题。

这次介绍的是ESP32 Arduino 直接读取蓝牙键盘的输入。特别需要注意的是蓝牙键盘有两种,Classical 和 BLE。我测试过罗技的 K480 是Classical蓝牙键盘:

还有苹果的A2449键盘,同样也是Classical 键盘。

IDF 提供了一个读取 Classical 键盘输入的示例,但是经过我的测试该代码无法正常工作。

这次介绍的代码只适用于 BLE 键盘,我入手的是雷柏 x220t 键鼠套装。这个键盘支持三种模式:2.4G、Classical 蓝牙和 BLE 蓝牙。

代码来自 https://github.com/esp32beans/BLE_HID_Client

/** NimBLE_Server Demo:
 *
 *  Demonstrates many of the available features of the NimBLE client library.
 *
 *  Created: on March 24 2020
 *      Author: H2zero
 *
*/
 
/*
 * This program is based on https://github.com/h2zero/NimBLE-Arduino/tree/master/examples/NimBLE_Client.
 * My changes are covered by the MIT license.
 */
 
/*
 * MIT License
 *
 * Copyright (c) 2022 esp32beans@gmail.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
 
// Install NimBLE-Arduino by h2zero using the IDE library manager.
#include <NimBLEDevice.h>
 
const char HID_SERVICE[] = "1812";
const char HID_INFORMATION[] = "2A4A";
const char HID_REPORT_MAP[] = "2A4B";
const char HID_CONTROL_POINT[] = "2A4C";
const char HID_REPORT_DATA[] = "2A4D";
 
void scanEndedCB(NimBLEScanResults results);
 
static NimBLEAdvertisedDevice* advDevice;
 
static bool doConnect = false;
static uint32_t scanTime = 0; /** 0 = scan forever */
 
 
/**  None of these are required as they will be handled by the library with defaults. **
 **                       Remove as you see fit for your needs                        */
class ClientCallbacks : public NimBLEClientCallbacks {
  void onConnect(NimBLEClient* pClient) {
    Serial.println("Connected");
    /** After connection we should change the parameters if we don't need fast response times.
     *  These settings are 150ms interval, 0 latency, 450ms timout.
     *  Timeout should be a multiple of the interval, minimum is 100ms.
     *  I find a multiple of 3-5 * the interval works best for quick response/reconnect.
     *  Min interval: 120 * 1.25ms = 150, Max interval: 120 * 1.25ms = 150, 0 latency, 60 * 10ms = 600ms timeout
     */
    pClient->updateConnParams(120,120,0,60);
  };
 
  void onDisconnect(NimBLEClient* pClient) {
    Serial.print(pClient->getPeerAddress().toString().c_str());
    Serial.println(" Disconnected - Starting scan");
    NimBLEDevice::getScan()->start(scanTime, scanEndedCB);
  };
 
  /** Called when the peripheral requests a change to the connection parameters.
   *  Return true to accept and apply them or false to reject and keep
   *  the currently used parameters. Default will return true.
   */
  bool onConnParamsUpdateRequest(NimBLEClient* pClient, const ble_gap_upd_params* params) {
    // Failing to accepts parameters may result in the remote device
    // disconnecting.
    return true;
  };
 
  /********************* Security handled here **********************
   ****** Note: these are the same return values as defaults ********/
  uint32_t onPassKeyRequest(){
    Serial.println("Client Passkey Request");
    /** return the passkey to send to the server */
    return 123456;
  };
 
  bool onConfirmPIN(uint32_t pass_key){
    Serial.print("The passkey YES/NO number: ");
    Serial.println(pass_key);
    /** Return false if passkeys don't match. */
    return true;
  };
 
  /** Pairing process complete, we can check the results in ble_gap_conn_desc */
  void onAuthenticationComplete(ble_gap_conn_desc* desc){
    if(!desc->sec_state.encrypted) {
      Serial.println("Encrypt connection failed - disconnecting");
      /** Find the client with the connection handle provided in desc */
      NimBLEDevice::getClientByID(desc->conn_handle)->disconnect();
      return;
    }
  };
};
 
/** Define a class to handle the callbacks when advertisments are received */
class AdvertisedDeviceCallbacks: public NimBLEAdvertisedDeviceCallbacks {
 
  void onResult(NimBLEAdvertisedDevice* advertisedDevice) {
    if ((advertisedDevice->getAdvType() == BLE_HCI_ADV_TYPE_ADV_DIRECT_IND_HD)
        || (advertisedDevice->getAdvType() == BLE_HCI_ADV_TYPE_ADV_DIRECT_IND_LD)
        || (advertisedDevice->haveServiceUUID() && advertisedDevice->isAdvertisingService(NimBLEUUID(HID_SERVICE))))
    {
      Serial.print("Advertised HID Device found: ");
      Serial.println(advertisedDevice->toString().c_str());
 
      /** stop scan before connecting */
      NimBLEDevice::getScan()->stop();
      /** Save the device reference in a global for the client to use*/
      advDevice = advertisedDevice;
      /** Ready to connect now */
      doConnect = true;
    }
  };
};
 
 
/** Notification / Indication receiving handler callback */
// Notification from 4c:75:25:xx:yy:zz: Service = 0x1812, Characteristic = 0x2a4d, Value = 1,0,0,0,0,
void notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify){
  std::string str = (isNotify == true) ? "Notification" : "Indication";
  str += " from ";
  /** NimBLEAddress and NimBLEUUID have std::string operators */
  str += std::string(pRemoteCharacteristic->getRemoteService()->getClient()->getPeerAddress());
  str += ": Service = " + std::string(pRemoteCharacteristic->getRemoteService()->getUUID());
  str += ", Characteristic = " + std::string(pRemoteCharacteristic->getUUID());
  str += ", Value = ";
  Serial.print(str.c_str());
  for (size_t i = 0; i < length; i++) {
    Serial.print(pData[i], HEX);
    Serial.print(',');
  }
  Serial.print(' ');
  if (length == 6) {
    // BLE Trackball Mouse from Amazon returns 6 bytes per HID report
    Serial.printf("buttons: %02x, x: %d, y: %d, wheel: %d",
        pData[0], *(int16_t *)&pData[1], *(int16_t *)&pData[3], (int8_t)pData[5]);
  }
  else if (length == 5) {
    // https://github.com/wakwak-koba/ESP32-NimBLE-Mouse
    // returns 5 bytes per HID report
    Serial.printf("buttons: %02x, x: %d, y: %d, wheel: %d hwheel: %d",
        pData[0], (int8_t)pData[1], (int8_t)pData[2], (int8_t)pData[3], (int8_t)pData[4]);
  }
  Serial.println();
}
 
/** Callback to process the results of the last scan or restart it */
void scanEndedCB(NimBLEScanResults results){
  Serial.println("Scan Ended");
}
 
 
/** Create a single global instance of the callback class to be used by all clients */
static ClientCallbacks clientCB;
 
 
/** Handles the provisioning of clients and connects / interfaces with the server */
bool connectToServer()
{
  NimBLEClient* pClient = nullptr;
 
  /** Check if we have a client we should reuse first **/
  if(NimBLEDevice::getClientListSize()) {
    /** Special case when we already know this device, we send false as the
     *  second argument in connect() to prevent refreshing the service database.
     *  This saves considerable time and power.
     */
    pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress());
    if(pClient){
      if(!pClient->connect(advDevice, false)) {
        Serial.println("Reconnect failed");
        return false;
      }
      Serial.println("Reconnected client");
    }
    /** We don't already have a client that knows this device,
     *  we will check for a client that is disconnected that we can use.
     */
    else {
      pClient = NimBLEDevice::getDisconnectedClient();
    }
  }
 
  /** No client to reuse? Create a new one. */
  if(!pClient) {
    if(NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
      Serial.println("Max clients reached - no more connections available");
      return false;
    }
 
    pClient = NimBLEDevice::createClient();
 
    Serial.println("New client created");
 
    pClient->setClientCallbacks(&clientCB, false);
    /** Set initial connection parameters: These settings are 15ms interval, 0 latency, 120ms timout.
     *  These settings are safe for 3 clients to connect reliably, can go faster if you have less
     *  connections. Timeout should be a multiple of the interval, minimum is 100ms.
     *  Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 51 * 10ms = 510ms timeout
     */
    pClient->setConnectionParams(12,12,0,51);
    /** Set how long we are willing to wait for the connection to complete (seconds), default is 30. */
    pClient->setConnectTimeout(5);
 
 
    if (!pClient->connect(advDevice)) {
      /** Created a client but failed to connect, don't need to keep it as it has no data */
      NimBLEDevice::deleteClient(pClient);
      Serial.println("Failed to connect, deleted client");
      return false;
    }
  }
 
  if(!pClient->isConnected()) {
    if (!pClient->connect(advDevice)) {
      Serial.println("Failed to connect");
      return false;
    }
  }
 
  Serial.print("Connected to: ");
  Serial.println(pClient->getPeerAddress().toString().c_str());
  Serial.print("RSSI: ");
  Serial.println(pClient->getRssi());
 
  /** Now we can read/write/subscribe the charateristics of the services we are interested in */
  NimBLERemoteService* pSvc = nullptr;
  NimBLERemoteCharacteristic* pChr = nullptr;
  NimBLERemoteDescriptor* pDsc = nullptr;
 
  pSvc = pClient->getService(HID_SERVICE);
  if(pSvc) {     /** make sure it's not null */
    // This returns the HID report descriptor like this
    // HID_REPORT_MAP 0x2a4b Value: 5,1,9,2,A1,1,9,1,A1,0,5,9,19,1,29,5,15,0,25,1,75,1,
    // Copy and paste the value digits to http://eleccelerator.com/usbdescreqparser/
    // to see the decoded report descriptor.
    pChr = pSvc->getCharacteristic(HID_REPORT_MAP);
    if(pChr) {     /** make sure it's not null */
      Serial.print("HID_REPORT_MAP ");
      if(pChr->canRead()) {
        std::string value = pChr->readValue();
        Serial.print(pChr->getUUID().toString().c_str());
        Serial.print(" Value: ");
        uint8_t *p = (uint8_t *)value.data();
        for (size_t i = 0; i < value.length(); i++) {
          Serial.print(p[i], HEX);
          Serial.print(',');
        }
        Serial.println();
      }
    }
    else {
      Serial.println("HID REPORT MAP char not found.");
    }
 
    // Subscribe to characteristics HID_REPORT_DATA.
    // One real device reports 2 with the same UUID but
    // different handles. Using getCharacteristic() results
    // in subscribing to only one.
    std::vector<NimBLERemoteCharacteristic*>*charvector;
    charvector = pSvc->getCharacteristics(true);
    for (auto &it: *charvector) {
      if (it->getUUID() == NimBLEUUID(HID_REPORT_DATA)) {
        Serial.println(it->toString().c_str());
        if (it->canNotify()) {
          if(!it->subscribe(true, notifyCB)) {
            /** Disconnect if subscribe failed */
            Serial.println("subscribe notification failed");
            pClient->disconnect();
            return false;
          }
        }
      }
    }
 
  }
  Serial.println("Done with this device!");
  return true;
}
 
void setup ()
{
  Serial.begin(115200);
 
  Serial.println("Starting NimBLE HID Client");
  /** Initialize NimBLE, no device name spcified as we are not advertising */
  NimBLEDevice::init("");
 
  /** Set the IO capabilities of the device, each option will trigger a different pairing method.
   *  BLE_HS_IO_KEYBOARD_ONLY    - Passkey pairing
   *  BLE_HS_IO_DISPLAY_YESNO   - Numeric comparison pairing
   *  BLE_HS_IO_NO_INPUT_OUTPUT - DEFAULT setting - just works pairing
   */
  //NimBLEDevice::setSecurityIOCap(BLE_HS_IO_KEYBOARD_ONLY); // use passkey
  //NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_YESNO); //use numeric comparison
 
  /** 2 different ways to set security - both calls achieve the same result.
   *  no bonding, no man in the middle protection, secure connections.
   *
   *  These are the default values, only shown here for demonstration.
   */
  NimBLEDevice::setSecurityAuth(true, false, true);
  //NimBLEDevice::setSecurityAuth(/*BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM |*/ BLE_SM_PAIR_AUTHREQ_SC);
 
  /** Optional: set the transmit power, default is 3db */
  NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */
 
  /** Optional: set any devices you don't want to get advertisments from */
  // NimBLEDevice::addIgnored(NimBLEAddress ("aa:bb:cc:dd:ee:ff"));
 
  /** create new scan */
  NimBLEScan* pScan = NimBLEDevice::getScan();
 
  /** create a callback that gets called when advertisers are found */
  pScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks());
 
  /** Set scan interval (how often) and window (how long) in milliseconds */
  pScan->setInterval(45);
  pScan->setWindow(15);
 
  /** Active scan will gather scan response data from advertisers
   *  but will use more energy from both devices
   */
  pScan->setActiveScan(true);
  /** Start scanning for advertisers for the scan time specified (in seconds) 0 = forever
   *  Optional callback for when scanning stops.
   */
  pScan->start(scanTime, scanEndedCB);
}
 
 
void loop ()
{
  /** Loop here until we find a device we want to connect to */
  if (!doConnect) return;
 
  doConnect = false;
 
  /** Found a device we want to connect to, do it now */
  if(connectToServer()) {
    Serial.println("Success! we should now be getting notifications!");
  } else {
    Serial.println("Failed to connect, starting scan");
    NimBLEDevice::getScan()->start(scanTime,scanEndedCB);
  }
}

烧录完成后,切换键盘到蓝牙模式,然后ESP32 S3 会和键盘配对,之后就可以读取到按键信息了:

Starting NimBLE HID Client
Advertised HID Device found: Name: RAPOO BT4.0 KB, Address: b4:ee:25:f3:86:99, appearance: 961, serviceUUID: 0x1812
Scan Ended
New client created
Connected
Connected to: b4:ee:25:f3:86:99
RSSI: -64
HID_REPORT_MAP 0x2a4b Value: 
Done with this device!
Success! we should now be getting notifications!
b4:ee:25:f3:86:99 Disconnected - Starting scan
Advertised HID Device found: Name: RAPOO BT4.0 KB, Address: b4:ee:25:f3:86:99, appearance: 961, serviceUUID: 0x1812
Scan Ended
Connected
Reconnected client
Connected to: b4:ee:25:f3:86:99
RSSI: -43
HID_REPORT_MAP 0x2a4b Value: 
Done with this device!
Success! we should now be getting notifications!
b4:ee:25:f3:86:99 Disconnected - Starting scan
Advertised HID Device found: Name: RAPOO BT4.0 KB, Address: b4:ee:25:f3:86:99, appearance: 961, serviceUUID: 0x1812
Scan Ended
Connected
Reconnected client
Connected to: b4:ee:25:f3:86:99
RSSI: -42
HID_REPORT_MAP 0x2a4b Value: 5,1,9,6,A1,1,85,1,5,7,19,E0,29,E7,15,0,25,1,75,1,95,8,81,2,95,1,75,8,81,3,95,5,75,1,5,8,19,1,29,5,91,2,95,1,75,3,91,3,95,6,75,8,15,0,26,FF,0,5,7,19,0,29,FF,81,0,C0,5,C,9,1,A1,1,85,2,15,0,25,1,75,1,95,1E,A,24,2,A,25,2,A,26,2,A,27,2,A,21,2,A,2A,2,A,23,2,A,8A,1,9,E2,9,EA,9,E9,9,CD,9,B7,9,B6,9,B5,A,83,1,A,94,1,A,92,1,A,9,2,9,B2,9,B3,9,B4,9,8D,9,4,9,30,A,7,3,A,A,3,A,B,3,A,B1,1,9,B8,81,2,95,1,75,2,81,3,C0,
Characteristic: uuid: 0x2a4d, handle: 27 0x001b, props:  0x1a
Characteristic: uuid: 0x2a4d, handle: 31 0x001f, props:  0x1a
Characteristic: uuid: 0x2a4d, handle: 35 0x0023, props:  0x0e
Done with this device!
Success! we should now be getting notifications!
Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,14,2B,0,0,0,0, 
Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,2B,0,0,0,0,0, 
Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,0,0,0,0,0,0, 
Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,14,0,0,0,0,0, 
Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,0,0,0,0,0,0, 
Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,2C,0,0,0,0,0,

CH567 USB0 Host 支持 BootProtocol Mouse

这是让 CH567 USB0 Host 支持 Boot Protocol 的鼠标,需要对设备发送 Get_Protocol 和 Set_Protocol。

基于之前的USB0_HOSTMS ,代码改动如下:

1.声明使用的 COMMAND

const UINT8 SetupClrFeature[] = { 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 };     //Clear feature
//LabzDebug_Start
const UINT8 SetupGetProtocol[] = { 0xA1, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 };    //GET Protocol
const UINT8 SetupSetProtocol[] = { 0x21, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };    //SET Protocol
//LabzDebug_End
__attribute__ ((aligned(4))) UINT8 UHBuffer0[U0H_MAXPACKET_LEN];    //数据发送缓存区

2.在EnumDevice( PUSBDEV pusbdev )函数中加入:

//LabzDebug_Start
//GET_PROTOCOL
        printf("Get Protocol..\n");
        CopySetupReqPkg( (PUINT8)SetupGetProtocol);
        printf("setup: ");    for(i=0; i<8; i++) printf("%02x ",  ((PUINT8)pSetupReq)[i]);   printf("\n");
        s = U0HCtrlTransfer( pusbdev, NULL, NULL, NULL );       // Ö´ÐпØÖÆ´«Êä
        if ( s != USB_INT_SUCCESS )     return( s );
printf("in: ");    for(i=0; i<len; i++)  printf("%02x ", UHRecvBuf[i]);  printf("\n");
//SET_PROTOCOL
        printf("Set Protocol\n");
        CopySetupReqPkg( (PUINT8)SetupSetProtocol );
printf("setup: ");    for(i=0; i<8; i++) printf("%02x ",  ((PUINT8)pSetupReq)[i]);   printf("\n");
        s = U0HCtrlTransfer( pusbdev, NULL, NULL, NULL );       // Ö´ÐпØÖÆ´«Êä
        if ( s != USB_INT_SUCCESS )     return( s );
//LabZDebug_End

USB0_HOST_BTMS下载

冷门的测试设备:MIPI CSI 信号测试设备

最近在研究 MIPI C-PHY 信号发生器,这个设备相比 USB 总线分析仪更加冷门。

MIPI是Mobile Industry Processor Interface 的缩写。MIPI协议实际上是一系列接口的协议,主要包含显示(DSI)、摄像头(CSI)等等。上图的设备是用于显示这个设备是用来产生 CSI MIPI 信号的。例如,我们的笔记本都会有摄像头,然后它通常位于盖子的上方,这样就需要通过线缆将CSI 信号从主板引到上方。这时候通常PM 会提出问题:经过了这么远的距离和好几个接头,是否会对摄像头成像质量有影响?如果确实有影响那么就必须通过增加Retimer或者Redriver的方式提升信号质量,在这种加钱的问题上 PM 是绝对不会放松的。使用这次的设备可以进行评估,在线缆的摄像头端连接上这个设备,假装有一个摄像头模组,通过发送不同的质量的信号,例如:1200mv或者780mv的MIPI信号,看看能否正常显示出来,从而得知线缆连接能够满足要求。

下面就是这个设备在测试过程中发送的信号,比如:加入抖动,改变信号电压等等。

使用方法也比较简单,首先请 Hardware 工程师连接输出端到线缆上;之后打开软件,选择型号

再选择测试类型,比如:自己编写一个或者打开已有的测试项目(正常情况下都是打开已有的测试项目,这是来自厂家,MIPI联盟的标准测试)

加载后点击 RUN 就可以进行测试了

测试会要求测试者和这个软件通过对话框进行交互,反馈当前被测试端显示是否正常。

最终生成Excel 格式的测试结果

参考:

1.这个型号设备的官网 https://introspect.ca/product/sv3c-cptx/

实现 Ch567 USB0 串口

上次我们在 CH567 的 USB1 上实现了 USB CDC 的功能,这一次尝试在 USB0上实现同样的同能。相比之前的程序,需要修改的位置有:

  1. \src\sys\CH56X_irq.c 中使用USB0DevIntDeal() 响应 USB 0 的中断
__attribute__( ( interrupt ( "id="XSTR(INT_ID_SATA) ) ) )void SATA_Handler(void)
{
	USB0DevIntDeal( );
}

2. \src\main\main.c 中打开 USB0 的中断

	Interrupt_init( 1<<INT_ID_USB0 );     /* 系统总中断开启 */

	USB0DeviceInit();			/* USB0Device Init */
	printf("USB0 Device Init!\n");

        while(1)
        {
                printf("Run\n");
                mDelaymS(5000);
                if (UsbConfig!=0)
                {
                        memcpy( UsbEp3INBuf, &Msg[0], sizeof( Msg ));
                        R16_UEP3_T_LEN1 =  sizeof( Msg );
                        R8_UEP3_TX_CTRL1 = (R8_UEP3_TX_CTRL1 & ~ MASK_UEP_T_RES) | UEP_T_RES_ACK;
                        while (R8_USB0_MIS_ST&bUMS_SIE_FREE==0) {}
                }
        };

3. ch56x_usb0dev372.h 中全部 USB1 替换为 USB0

4. ch56x_usb0dev372.c 中全部 USB1 替换为 USB0

CopperCube 配合 FireBeetle 改变球体颜色

这次使用 CopperCube 制作2个球体,然后可以通过 FireBeetle 控制这两个球体的颜色。
1.创建一个新的场景,删除场景中自带的立方体,然后创建一个球体(Sphere)

2.新建的球体是自带贴图的,这个贴图来自前面立方

3.选中球体,在Textures 中选择第一个空贴图,然后在属性的 Materials 中点击更换贴图

4.之后球体上面的贴图就为空了

5.为了便于观察,我们给图赋予一个颜色,选中物体后右键,在弹出菜单中 选择 “Modify Selection”->”Set vertex Colors”。 在弹出的调色板上选择你喜欢的颜色

6.球体变成了红色,选中球体后再使用右键调出菜单进行: clone

7.现在场景中有2个红色球体了,为了便于观察,改成动态光照,在Materials 中选择 Dynamic

8.在场景中创景一个光源

9.让光源动起来,具体方法在上次的文章中介绍过

10.之后保存场景为FBTest.ccb文件

11.编写一个响应键盘的JavaScripe 文档,当收到不同的按键时,改变球体的颜色。文件命名为 FBTest.js 放到和上面 FBTest.ccb 同一个目录下

// register key events
ccbRegisterKeyDownEvent("keyPressedDown");

function keyPressedDown(keyCode)
{
	//z
	if (keyCode == 90)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh1");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x00ff0000);
		} 
	}

	//x
	if (keyCode == 88)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh1");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x0000FF00);
		} 

	}


	//c
	if (keyCode == 67)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh2");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x0000FF00);
		} 
	}


	//v
	if (keyCode == 86)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh2");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x000000FF);
		} 

	}
	
	print(keyCode );
}
  1. 编写FireBeetle 代码,我们需要使用 FireBeetle 的蓝牙功能,将其模拟为一个蓝牙键盘,当有不同按键按下后,发送按键信息。这样当CopperCube 生成的 EXE 收到后,会改变颜色
  2. 编译之后就能看到最终的结果了。
/**
   This example turns the ESP32 into a Bluetooth LE keyboard that writes the words, presses Enter, presses a media key and then Ctrl+Alt+Delete
*/
#include <BleKeyboard.h>
#define PINA 12
#define PINB  4
#define PINC 16
#define PIND 17

BleKeyboard bleKeyboard;

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE work!");
  pinMode(PINA, INPUT_PULLUP);
  pinMode(PINB, INPUT_PULLUP);
  pinMode(PINC, INPUT_PULLUP);
  pinMode(PIND, INPUT_PULLUP);
  bleKeyboard.begin();
}

void loop() {
  if (bleKeyboard.isConnected()) {
    if (digitalRead(PINA) == LOW) {
      Serial.println("Sending 'z'");
      bleKeyboard.print("z");
      delay(200);
    }
    if (digitalRead(PINB) == LOW) {
      Serial.println("Sending 'x'");
      bleKeyboard.print("x");
      delay(200);
    }

    if (digitalRead(PINC) == LOW) {
      Serial.println("Sending 'c'");
      bleKeyboard.print("c");
      delay(200);
    }
    if (digitalRead(PIND) == LOW) {
      Serial.println("Sending 'v'");
      bleKeyboard.print("v");
      delay(200);
    }
  }
}

CH567 实现MIDI 设备

使用 Lufa 的示例,作为 MIDI 的参考:

USB Composite Device

  Connection Status    Device connected  
  Current Configuration    1  
  Speed    Full (12 Mbit/s)  
  Device Address    4  
  Number Of Open Pipes    2  

Device Descriptor LUFAMIDI Demo

  Offset    Field    Size    Value    Description  
  0    bLength    1    12h  
  1    bDescriptorType    1    01h    Device  
  2    bcdUSB    2    0110h    USB Spec 1.1  
  4    bDeviceClass    1    00h    Class info in Ifc  Descriptors  
  5    bDeviceSubClass    1    00h  
  6    bDeviceProtocol    1    00h  
  7    bMaxPacketSize0    1    08h    8 bytes  
  8    idVendor    2    03EBh  
  10    idProduct    2    2048h  
  12    bcdDevice    2    0001h    0.01  
  14    iManufacturer    1    01h    “Dean  Camera”  
  15    iProduct    1    02h    “LUFA MIDI  Demo”  
  16    iSerialNumber    1    00h  
  17    bNumConfigurations    1    01h  

Configuration Descriptor1

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    02h    Configuration  
  2    wTotalLength    2    0065h  
  4    bNumInterfaces    1    02h  
  5    bConfigurationValue    1    01h  
  6    iConfiguration    1    00h  
  7    bmAttributes    1    C0h    Self Powered  
  4..0: Reserved    …00000  
  5: Remote Wakeup    ..0…..    No  
  6: Self Powered    .1……    Yes  
  7: Reserved (set to  one)
  (bus-powered for 1.0)  
  1…….  
  8    bMaxPower    1    32h    100 mA  

Interface Descriptor 0/0 Audio,0 Endpoints

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    04h    Interface  
  2    bInterfaceNumber    1    00h  
  3    bAlternateSetting    1    00h  
  4    bNumEndpoints    1    00h  
  5    bInterfaceClass    1    01h    Audio  
  6    bInterfaceSubClass    1    01h    Audio Control  
  7    bInterfaceProtocol    1    00h  
  8    iInterface    1    00h  

Audio Control InterfaceHeader Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    24h    Audio Control  Interface Header  
  2    7    01 00 01 09 00 01 01  

Interface Descriptor 1/0 Audio,2 Endpoints

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    04h    Interface  
  2    bInterfaceNumber    1    01h  
  3    bAlternateSetting    1    00h  
  4    bNumEndpoints    1    02h  
  5    bInterfaceClass    1    01h    Audio  
  6    bInterfaceSubClass    1    03h    MIDI Streaming  
  7    bInterfaceProtocol    1    00h  
  8    iInterface    1    00h  

MIDI Streaming InterfaceHeader Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    07h  
  1    bDescriptorType    1    24h    MIDI Streaming  Interface Header  
  2    5    01 00 01 41 00  

MIDI In Jack Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    06h  
  1    bDescriptorType    1    24h    MIDI In Jack  
  2    4    02 01 01 00  

MIDI In Jack Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    06h  
  1    bDescriptorType    1    24h    MIDI In Jack  
  2    4    02 02 02 00  

MIDI Out Jack Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    24h    MIDI Out Jack  
  2    7    03 01 03 01 02 01 00  

MIDI Out Jack Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    24h    MIDI Out Jack  
  2    7    03 02 04 01 01 01 00  

Endpoint Descriptor 01 1Out, Bulk, 64 bytes

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    05h    Endpoint  
  2    bEndpointAddress    1    01h    1 Out  
  3    bmAttributes    1    02h    Bulk  
  1..0: Transfer Type    ……10    Bulk  
  7..2: Reserved    000000..  
  4    wMaxPacketSize    2    0040h    64 bytes  
  6    bInterval    1    05h  
  7    bRefresh    1    00h  
  8    bSynchAddress    1    00h  

Unrecognized AudioClass-Specific Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    05h  
  1    bDescriptorType    1    25h    Unrecognized Audio  Class-Specific  
  2    3    01 01 01  

Endpoint Descriptor 82 2In, Bulk, 64 bytes

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    05h    Endpoint  
  2    bEndpointAddress    1    82h    2 In  
  3    bmAttributes    1    02h    Bulk  
  1..0: Transfer Type    ……10    Bulk  
  7..2: Reserved    000000..  
  4    wMaxPacketSize    2    0040h    64 bytes  
  6    bInterval    1    05h  
  7    bRefresh    1    00h  
  8    bSynchAddress    1    00h  

Unrecognized AudioClass-Specific Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    05h  
  1    bDescriptorType    1    25h    Unrecognized Audio  Class-Specific  
  2    3    01 01 03  
This report was generated by USBlyzer

代码上和之前的串口非常类似(MIDI 可以看作是波特率特殊的串口):

Step to UEFI (266)Setup 界面添加字符的实验


最近有一个有趣的想法:如何在 Setup 界面上添加字符,比如:增加 www.lab-z.com这个字样。

经过在 EDK2 代码中搜索,在MdeModulePkg\Library\CustomizedDisplayLib\CustomizedDisplayLibInternal.c 文件中找到如下函数:

/**
  Print framework and form title for a page.
 
  @param[in]  FormData             Form Data to be shown in Page
**/
VOID
PrintFramework (
  IN FORM_DISPLAY_ENGINE_FORM  *FormData
  )

Setup界面是在这里进行绘制的,最简单的修改就是在绘制完成之后写上需要的字符。

/**
  Print framework and form title for a page.
 
  @param[in]  FormData             Form Data to be shown in Page
**/
VOID
PrintFramework (
  IN FORM_DISPLAY_ENGINE_FORM  *FormData
  )
{
  UINTN   Index;
  CHAR16  Character;
  CHAR16  *Buffer;
  UINTN   Row;
  CHAR16  *TitleStr;
  UINTN   TitleColumn;
  CHAR16  StrBuffer[]=L"WWW.LAB-Z.COM";
 
  if (gClassOfVfr != FORMSET_CLASS_PLATFORM_SETUP) {
    //
    // Only Setup page needs Framework
    //
    ClearLines (
      gScreenDimensions.LeftColumn,
      gScreenDimensions.RightColumn,
      gScreenDimensions.BottomRow - STATUS_BAR_HEIGHT - gFooterHeight,
      gScreenDimensions.BottomRow - STATUS_BAR_HEIGHT - 1,
      KEYHELP_TEXT | KEYHELP_BACKGROUND
      );
    return;
  }
…………………………………………….
…………………………………………….
…………………………………………….
  Character = BOXDRAW_UP_RIGHT;
  PrintCharAt (gScreenDimensions.LeftColumn, gScreenDimensions.BottomRow - STATUS_BAR_HEIGHT - 1, Character);
 
  PrintStringAt ((UINTN)-1, (UINTN)-1, Buffer);
 
  Character = BOXDRAW_UP_LEFT;
  PrintCharAt ((UINTN)-1, (UINTN)-1, Character);
 
  FreePool (Buffer);
   
  //LABZ_Debug_Start
  PrintStringAt (1, 0, StrBuffer);
  //LABZ_Debug_End
}

运行结果:

左上角出现我们设定的字符

Intel GNA 介绍

在安装驱动的时候,我们通常会看到Intel GNA 设备或者它的驱动,为了更深入的了解这个功能抽空研究了一下。

“Intel GNA”是“Intel Gaussian & Neural Accelerator” 的缩写,翻译过来是 “英特尔高斯和神经加速器”。这是一个AI 加速IP,简单的说在AI 计算中会有一些常见的算法,如果用 CPU 进行会占用大量的 CPU资源,于是 Intel 将这部分分离出来专门定制了一个IP,如果有这方面的需求那么直接将数据丢给这个IP进行处理能够节省CPU资源,特别体现在省电上。

与之类似,很久之前的 Intel 80386只擅长整数运算,虽然能够使用软件编程的方式模拟浮点,但是速度慢功耗高,于是就有了“数字协处理器”80387。二者搭配起来,当386碰到浮点运算就交给387进行,这样速度更快。

Intel GNA 也是一个类似的设备,目前的主要应用是声音处理,比如:在进行网络会议时,用于消除麦克风中的背景噪音。

看起来这个功能离用户很近,离BIOS很远,是一个好功能。

参考:

  1. https://www.sohu.com/a/418237849_114822
  2. https://www.intel.cn/content/www/cn/zh/products/docs/processors/core/12th-gen-core-desktop-brief.html
  3. https://newsroom.intel.cn/news-releases/11th-gen-tiger-lake-evo/

使用 CH567 实现 USB1 串口

这次的目标是实现一个 USB 转串口的设备,参考的是Arduino Leonardo 的 USB CDC。这个串口是标准USB串口,在Windows 下无需驱动。首先抓取描述符如下:

USB Composite Device

Connection StatusDevice connected
Current Configuration1
SpeedFull (12 Mbit/s)
Device Address4
Number Of Open Pipes3

Device Descriptor Arduino Leonardo

OffsetFieldSizeValueDescription
0bLength112h
1bDescriptorType101hDevice
2bcdUSB20200hUSB Spec 2.0
4bDeviceClass1EFhMiscellaneous
5bDeviceSubClass102hCommon Class
6bDeviceProtocol101hInterface Association Descriptor
7bMaxPacketSize0140h64 bytes
8idVendor22341h
10idProduct28036h
12bcdDevice20100h1.00
14iManufacturer101h“Arduino LLC”
15iProduct102h“Arduino Leonardo”
16iSerialNumber103h
17bNumConfigurations101h

Configuration Descriptor 1 Bus Powered, 500 mA

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType102hConfiguration
2wTotalLength2004Bh
4bNumInterfaces102h
5bConfigurationValue101h
6iConfiguration100h
7bmAttributes1A0hBus Powered, Remote Wakeup
4..0: Reserved…00000 
5: Remote Wakeup..1….. Yes
6: Self Powered.0…… No, Bus Powered
7: Reserved (set to one)
(bus-powered for 1.0)
1……. 
8bMaxPower1FAh500 mA

Interface Association Descriptor Abstract Control Model

OffsetFieldSizeValueDescription
0bLength108h
1bDescriptorType10BhInterface Association
2bFirstInterface100h
3bInterfaceCount102h
4bFunctionClass102hCDC Control
5bFunctionSubClass102hAbstract Control Model
6bFunctionProtocol100h
7iFunction100h

Interface Descriptor 0/0 CDC Control, 1 Endpoint

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType104hInterface
2bInterfaceNumber100h
3bAlternateSetting100h
4bNumEndpoints101h
5bInterfaceClass102hCDC Control
6bInterfaceSubClass102hAbstract Control Model
7bInterfaceProtocol100h
8iInterface100h

Header Functional Descriptor

OffsetFieldSizeValueDescription
0bFunctionLength105h
1bDescriptorType124hCS Interface
2bDescriptorSubtype100hHeader
3bcdCDC20110h1.10

Call Management Functional Descriptor

OffsetFieldSizeValueDescription
0bFunctionLength105h
1bDescriptorType124hCS Interface
2bDescriptorSubtype101hCall Management
3bmCapabilities101h
7..2: Reserved000000.. 
1: Data Ifc Usage……0. Call management only over Comm Ifc
0: Call Management…….1 Handles call management itself
4bDataInterface101h

Abstract Control Management Functional Descriptor

OffsetFieldSizeValueDescription
0bFunctionLength104h
1bDescriptorType124hCS Interface
2bDescriptorSubtype102hAbstract Control Management
3bmCapabilities106h
7..4: Reserved0000…. 
3: Connection….0… 
2: Send Break…..1.. Send Break request supported
1: Line Coding……1. Line Coding requests and Serial State notification supported
0: Comm Features…….0 

Union Functional Descriptor

OffsetFieldSizeValueDescription
0bFunctionLength105h
1bDescriptorType124hCS Interface
2bDescriptorSubtype106hUnion
3bControlInterface100h
4bSubordinateInterface0101hCDC Data

Endpoint Descriptor 81 1 In, Interrupt, 64 ms

OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress181h1 In
3bmAttributes103hInterrupt
1..0: Transfer Type……11 Interrupt
7..2: Reserved000000.. 
4wMaxPacketSize20010h16 bytes
6bInterval140h64 ms

Interface Descriptor 1/0 CDC Data, 2 Endpoints

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType104hInterface
2bInterfaceNumber101h
3bAlternateSetting100h
4bNumEndpoints102h
5bInterfaceClass10AhCDC Data
6bInterfaceSubClass100h
7bInterfaceProtocol100h
8iInterface100h

Endpoint Descriptor 02 2 Out, Bulk, 64 bytes

OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress102h2 Out
3bmAttributes102hBulk
1..0: Transfer Type……10 Bulk
7..2: Reserved000000.. 
4wMaxPacketSize20040h64 bytes
6bInterval100h

Endpoint Descriptor 83 3 In, Bulk, 64 bytes

OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress183h3 In
3bmAttributes102hBulk
1..0: Transfer Type……10 Bulk
7..2: Reserved000000.. 
4wMaxPacketSize20040h64 bytes
6bInterval100h

This report was generated by USBlyzer

实现了上面的描述符之后,就能保证插入系统后 Windows 设备管理器上不会出现惊叹号。Windows 支持的标准 CDC 动作有下面8个【参考1】

  1. SET_LINE_CODING  用于主机对设备设置波特率,停止位,奇偶校验和位数
  2. GET_LINE_CODING用于主机取得设备当前波特率,停止位,奇偶校验和位数
  3. SET_CONTROL_LINE_STATE 用于产生 RS-232/V.24 标准的控制信号
  4. SEND_BREAK
  5. SERIAL_STATE  返回状态信息,比如:奇偶校验错误
  6. SEND_ENCAPSULATED_COMMAND
  7. GET_ENCAPSULATED_RESPONSE
  8. RESPONSE_AVAILABLE

从实际验证的结果看起来(就是前面提到的使用 Arduino Leonardo 作为验证对象),实现 1-3 的支持外加 2个Endpoint Bulk传输即可实现通讯。

 1.SET_LINE_CODING  的实现。收到 bRequestType ==0x21, bRequest== SET_LINE_CODING  即可判定这个操作;之后用 ENDPOINT 0 的OUT 中返回当前的LineInfo;最后再通过 ENDPOINT 0 的 IN 返回0字节

2. GET_LINE_CODING  的实现。收到 bRequestType ==0xA1, bRequest== GET_LINE_CODING  即可判定这个操作;之后直接返回当前的LineInfo;最后再通过 ENDPOINT 0 的 IN 返回0字节

3. SET_CONTROL_LINE_STATE 的实现。收到 bRequestType ==0x21, bRequest== 0x22  即可判定这个操作;之后直接通过ENDPOINT 0 的 IN 返回0字节。

实现上面的操作之后,即可使用串口工具打开设备产生的串口了。接下来实现串口传输的模拟:

  1. 从Windows(HOST) 对CH567通过串口工具发送数据。数据会出现在 endpoint2 OUT上,我们将收到的数据送到CH567的串口上,然后再通过一个额外的串口转USB即可看到。具体代码是:
                        if(intstatus == (UIS_TOKEN_OUT|2))             /* endpoint 2 下传 */
                        {
                                if(R8_USB1_INT_ST&bUIS_TOG_OK)
                                {

                                        // 下传是 HOST -> DEVICE
                                        // 用串口工具打开设备对应的串口,然输入的内容可以在 Debug 串口上看到
                                        for (i=0; i<R16_USB1_RX_LEN; i++)
                                        {
                                                printf("%X ",UsbEp2OUTBuf[i]);
                                        }
                                        printf("\n");
                                }
                        }

2.从CH567定时对 Windows 发送字符串,使用串口工具打开CH567端口后可以看到这个字符串。修改有2处,第一个是发送的代码,在main.c 中每隔5秒发送一次:

        while(1)
        {
                mDelaymS(5000);
                if (UsbConfig!=0)
                {
                        memcpy( UsbEp3INBuf, &Msg[0], sizeof( Msg ));
                        R16_UEP3_T_LEN1 =  sizeof( Msg );
                        R8_UEP3_TX_CTRL1 = (R8_UEP3_TX_CTRL1 & ~ MASK_UEP_T_RES) | UEP_T_RES_ACK;
                }
        };

另外一处是当CH567收到 Endpoint3 IN 中断时,使用0字节来回复给主机

 if(intstatus == (UIS_TOKEN_IN|3))             /* endpoint 3 上传 */
                        {
                                R16_UEP3_T_LEN1 =  0;
                                R8_UEP3_TX_CTRL1 = (R8_UEP3_TX_CTRL1 & ~ MASK_UEP_T_RES) | UEP_T_RES_ACK;
                        }

此外,还有一处需要特别注意的是:必须使用高波特率用于 printf 的串口输出(>1Mhz),实验中我使用的是 CH343 6Mhz的波特率,否则会发生丢失log的情况(实际上有跑到代码,但是对应那句话的 Log 不出现,这个问题我调试了2天,在USB 逻辑分析仪上看到了发送的数据包,但是串口 Log说没有)。

运行结果如下,左侧是用于调试的CH343产生的串口,右边是CH567模拟出来的串口。当我们对CH567发送”1234567”时,CH567收到后会从UART再次送出,因此我们在左侧能看到;此外,CH567每隔5秒发送一次”www.lab-z.com”字符串在右侧窗口可以看到。

完整代码下载:

参考:

1. https://www.silabs.com/documents/public/application-notes/AN758.pdf