生成小学二年级数学加减运算练习题的代码

最近想找点题目给娃做,然后发现Baidu搜索到的大部分都是收费的,于是自己编写代码进行生成。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Management;
using System.IO;
 
namespace _100Calc
{
    class Program
    {
        //将List转换为TXT文件
        static public void WriteListToTextFile(List<string> list, string txtFile)
        {
            //创建一个文件流,用以写入或者创建一个StreamWriter 
            FileStream fs = new FileStream(txtFile, FileMode.OpenOrCreate, FileAccess.Write);
            StreamWriter sw = new StreamWriter(fs);
            sw.Flush();
            // 使用StreamWriter来往文件中写入内容 
            sw.BaseStream.Seek(0, SeekOrigin.Begin);
            for (int i = 0; i < list.Count; i++) sw.WriteLine(list[i]);
            //关闭此文件t 
            sw.Flush();
            sw.Close();
            fs.Close();
        }
 
 
        //读取文本文件转换为List 
        static public List<string> ReadTextFileToList(string fileName)
        {
            FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
            List<string> list = new List<string>();
            StreamReader sr = new StreamReader(fs);
            //使用StreamReader类来读取文件 
            sr.BaseStream.Seek(0, SeekOrigin.Begin);
            // 从数据流中读取每一行,直到文件的最后一行
            string tmp = sr.ReadLine();
            while (tmp != null)
            {
                list.Add(tmp);
                tmp = sr.ReadLine();
            }
            //关闭此StreamReader对象 
            sr.Close();
            fs.Close();
            return list;
        }
 
        static void Main(string[] args)
        {
            List<string> Add100 = new List<string>();
             
            for (int i = 10; i < 100; i++)
                for (int j = 10; j < 100; j++) {
                    //Console.WriteLine("{0}+{1}=", i, j);
                    Add100.Add(i.ToString() + "+" + j.ToString() + "=");
                }
            //WriteListToTextFile(Add100,"100Add.txt");
 
            List<string> Sub100 = new List<string>();
            for (int i = 99; i >10; i--)
                for (int j = i-1; j >10; j--)
                {
                    //Console.WriteLine("{0}-{1}=", i, j);
                    Sub100.Add(i.ToString() + "-" + j.ToString() + "=");
                }
 
            //A - B + C 形式的,特别注意 A-B>0 
            List<string> ThrCalc1 = new List<string>();
            for (int i = 99; i > 10; i--)
                for (int j = i - 1; j > 10; j--)
                    for (int k = 10; k<100; k++)
                    {
                        //Console.WriteLine("{0}-{1}+{2}=", i, j,k);
                        ThrCalc1.Add(i.ToString() + "-" + j.ToString() + "+" + k.ToString() + "=");
                }
 
            //A - B - C 形式的,特别注意 A-B-C>0  
            List<string> ThrCalc2 = new List<string>();
            for (int i = 99; i > 10; i--)
                for (int j = i - 1; j > 10; j--)
                    for (int k = 10; k < 100; k++)
                        {
                            if (i-j-k>0)
                            {
                                //Console.WriteLine("{0}-{1}+{2}=", i, j,k);
                                ThrCalc2.Add(i.ToString() + "-" + j.ToString() + "-" + k.ToString() + "=");
                            }
                         
                    }
 
            Random rnd = new Random();
            List<string> timu = new List<string>();
 
            // 生成 180 套题目
            for (int i = 0; i < 160; i++) {
                 
                for (int q1 = 0; q1 < 21; q1++) {
                    int v = rnd.Next(Add100.Count);
                    timu.Add(Add100[v]);
                    Add100.RemoveAt(v);
                }
                for (int q2 = 0; q2 < 22; q2++)
                {
                    int v = rnd.Next(Sub100.Count);
                    timu.Add(Sub100[v]);
                    Sub100.RemoveAt(v);
                }
                for (int q3 = 0; q3 < 7; q3++)
                {
                    int v = rnd.Next(ThrCalc1.Count);
                    timu.Add(ThrCalc1[v]);
                    ThrCalc1.RemoveAt(v);
                }
                for (int q4 = 0; q4 < 7; q4++)
                {
                    int v = rnd.Next(ThrCalc2.Count);
                    timu.Add(ThrCalc2[v]);
                    ThrCalc2.RemoveAt(v);
                }
 
                 
                // 打乱排列顺序
                string s;
                for (int j = 0; j < timu.Count; j++) {
                    int v = rnd.Next(timu.Count);
                    s = timu[v];
                    timu[v] = timu[j];
                    timu[j] = s;
                }
                 
                // 输出
                for (int j = 0; j < timu.Count/3; j++)
                {
                    Console.WriteLine("{0,-10}\t\t\t{1,-10}\t\t\t{2,-10}", timu[j*3], timu[j*3+1], timu[j*3+2]);
                }
                //Console.WriteLine("Data:                      Score:        ");
                timu.Clear();
            }
 
            Console.WriteLine("{0}  {1}  {2} {3}", Add100.Count,Sub100.Count, ThrCalc1.Count, ThrCalc2.Count);
            Console.ReadLine();
        }
    }
}

每一页21道2位数加法,22道两位数减法(没有负数),还有14到3个两位数加减运算。一共160页。直接拷贝到 Word文章后,生成一份 PDF如下,有需要的朋友可以下载打印。

增加一个3个数字的加减乘运算

一年级二十以内加减题目

https://www.lab-z.com/wp-content/uploads/2022/11/100Calc.pdf

增加一个3个数字的加减乘运算

https://www.lab-z.com/wp-content/uploads/2023/01/3Calc.pdf

一年级二十以内加减题目

https://www.lab-z.com/wp-content/uploads/2023/01/Grade1.pdf

硬盘清理神器: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/