EasyX 用点画圆

圆形的参数方程如下:

根据这个,编写 EasyX 代码如下:

#include <graphics.h>  // EasyX图形库头文件
#include <conio.h>     // 用于_getch()
#include <math.h>    

#define RAD 100
int main()
{
    int x,y;
    // 初始化640x480像素的图形窗口
    initgraph(640, 480);

    for (int i = 0; i < 360; i++) {
        x = 320 + RAD * sin(i * 3.1415 / 180); // sin 用弧度做参数
        y=  240+ RAD * cos(i * 3.1415 / 180); // cos 用弧度做参数
        putpixel(x, y, RED);  
    }

    // 保持窗口显示
    _getch();

    // 关闭图形窗口
    closegraph();
    return 0;
}

运行结果:

此外,这里再给出一个奇怪/低效的算法。基于如下限制:所有的点在八个方向上只存在两个点与之相近(换句话,一个像素的线绘制出来的圆形),基于此限制,设计算法如下:

  1. 给出起始点坐标(Xorg,Yorg);
  2. 生成8个方向的坐标,计算这些点位和圆心的距离,然后选择最接近半径的作为下一个点的坐标;
  3. 重复上述动作计算出所有的点(不过我还没有想明白怎么通过公式计算出点的数量)
#include <graphics.h>  // EasyX图形库头文件
#include <conio.h>     // 用于_getch()
#include <math.h>    
//#include <stdio.h>  

#define Xcenter 320
#define Ycenter 240
#define RAD 100

// 计算 (x,y) 到圆心的距离
double CalculateDistance(int x, int y)
{
    return (sqrt((x - Xcenter) * (x - Xcenter) + (y - Ycenter) * (y - Ycenter)));
}

// 找到下一个点位
// 输入当前点位坐标 (Xcurrent,Ycurrent)
// 前一个点位坐标 (Xlast,Ylast
void FindNextPoint(int Xcurrent, int Ycurrent, int Xlast, int Ylast, int *Xnext, int *Ynext)
{
    double gap= 1000000000.0;
    double tmp;

    // 左
    if ((Xcurrent - 1 != Xlast) || (Ycurrent != Ylast)) {
        tmp = CalculateDistance(Xcurrent - 1, Ycurrent);
        tmp = fabs(tmp - RAD);
        if (gap > tmp) {
            gap = fabs(CalculateDistance(Xcurrent - 1, Ycurrent) - RAD);
            *Xnext = Xcurrent - 1;
            *Ynext = Ycurrent;
        }
    }
    // 左上
    if ((Xcurrent-1 != Xlast) || (Ycurrent-1!= Ylast)) {
        if (gap > fabs(CalculateDistance(Xcurrent - 1, Ycurrent-1) - RAD)) {
            gap = fabs(CalculateDistance(Xcurrent - 1, Ycurrent-1) - RAD);
            *Xnext = Xcurrent - 1;
            *Ynext = Ycurrent-1; 
        }
    }
    // 上
    if ((Xcurrent!= Xlast) || (Ycurrent - 1 != Ylast)) {
        if (gap > fabs(CalculateDistance(Xcurrent, Ycurrent - 1) - RAD)) {
            gap = fabs(CalculateDistance(Xcurrent, Ycurrent - 1) - RAD);
            *Xnext = Xcurrent;
            *Ynext = Ycurrent - 1;
        }
    }
    // 右上
    if ((Xcurrent+1 != Xlast) || (Ycurrent - 1 != Ylast)) {
        if (gap > fabs(CalculateDistance(Xcurrent+1, Ycurrent - 1) - RAD)) {
            gap = fabs(CalculateDistance(Xcurrent+1, Ycurrent - 1) - RAD);
            *Xnext = Xcurrent+1;
            *Ynext = Ycurrent - 1;
        }
    }
    // 右
    if ((Xcurrent + 1 != Xlast) || (Ycurrent != Ylast)) {
        if (gap > fabs(CalculateDistance(Xcurrent + 1, Ycurrent) - RAD)) {
            gap = fabs(CalculateDistance(Xcurrent + 1, Ycurrent) - RAD);
            *Xnext = Xcurrent + 1;
            *Ynext = Ycurrent;
        }
    }
    // 右下
    if ((Xcurrent + 1 != Xlast) || (Ycurrent+1 != Ylast)) {
        if (gap > fabs(CalculateDistance(Xcurrent + 1, Ycurrent+1) - RAD)) {
            gap = fabs(CalculateDistance(Xcurrent + 1, Ycurrent+1) - RAD);
            *Xnext = Xcurrent + 1;
            *Ynext = Ycurrent + 1;
        }
    }
    // 下
    if ((Xcurrent != Xlast) || (Ycurrent +1!= Ylast)) {
        if (gap > fabs(CalculateDistance(Xcurrent, Ycurrent+1) - RAD)) {
            gap = fabs(CalculateDistance(Xcurrent, Ycurrent+1) - RAD);
            *Xnext = Xcurrent;
            *Ynext = Ycurrent+1;
        }
    }
    // 左下
    if ((Xcurrent-1 != Xlast) || (Ycurrent + 1 != Ylast)) {
        if (gap > fabs(CalculateDistance(Xcurrent-1, Ycurrent + 1) - RAD)) {
            gap = fabs(CalculateDistance(Xcurrent-1, Ycurrent + 1) - RAD);
            *Xnext = Xcurrent-1;
            *Ynext = Ycurrent + 1;
        }
    }

}
int main()
{
    int x=0, y= 0;
    int Xlast = Xcenter - RAD, Ylast = Ycenter;
    int Xcurrent = Xcenter - RAD, Ycurrent = Ycenter;

    // 初始化640x480像素的图形窗口
    initgraph(640, 480);


    for (int i = 0; i < 800; i++) {
        FindNextPoint(Xcurrent, Ycurrent,Xlast,Ylast,&x,&y);

        //printf("%d %d\n", x, y);
        putpixel(Xcurrent, Ycurrent, RED);
        Xlast = Xcurrent;
        Ylast = Ycurrent;
        Xcurrent = x;
        Ycurrent = y;
    }

    // 保持窗口显示
    _getch();

    // 关闭图形窗口
    closegraph();
    return 0;
}

Step to memory 004 单通道和双通道

继续 《PC 内存的秘密 第一部分》原文在 https://www.bit-tech.net/reviews/tech/memory/the_secrets_of_pc_memory_part_1/6/

有几个独特但重要的速度概念需要牢记。它们是 DRAM 核心频率、输入输出 (IO) 缓冲区频率、内存总线频率和数据频率。它们用于描述内存系统不同部件的性能水平。

所有计算机内存模块都以数据频率为标称值。例如,DDR2-800 中的“800”数字描述了模块以 800MHz频率搬运数据的能力。另一方面,IO 缓冲区和总线频率将以 400MHz 运行,而 DRAM 核心频率仅以 200MHz 运行。

频率 (MHz) 和数据吞吐量 (Mbps) 之间的关系很简单。一个信号是 1 位数据:0 或 1。下图中的圆形黑色“操作点”。800MHz 数据频率意味着它将以每秒 8 亿次的速度发送 1 位数据,因此两者的乘积等于每秒 800 兆比特 (Mbps)。 (注:请记住,位和字节是不同的:8 位 = 1 字节,因此之前的 800Mbps 仅相当于 100MBps 或每秒兆字节)。

在给定数据频率的情况下,实际或有效数据吞吐量低于预期。例如,800MHz 的 DDR2 不会将该频率全部用于数据流。相反,命令和控制信号会占用部分数据。这有点像购买 160GB 硬盘,格式化后,只有 149GB 可用于数据。这是因为驱动器的一部分被系统级信息(如主引导记录 MBR 和分区表)占用。

对于一些爱好者和完美主义者来说,这是内存超频很好的理由,以使数据吞吐量更接近或超过 800MHz 数据流。使用指定为 1066MHz 的 DDR2 模块可以更好地实现这一点。

双通道和单通道模式

DDR 的另一项重大创新是能够使用双通道,而不是传统的单通道内存总线。这种设计极大地提高了内存性能。
在对称双通道芯片组上,将两个内存模块放在相同颜色的插槽中将自动为用户提供双通道性能,但是在 4 插槽主板上使用 3 个内存模块将使主板切换回单通道模式。非对称双通道芯片组能够使用 3 个 DIMM 在双通道模式下运行,从而始终有效地为用户提供 128 位内存性能。

当前基于台式机的 DDR 内存技术无法支持每通道超过 2 个 DIMM,但基于服务器和工作站的 FB-DIMM(全缓冲 DIMM)内存控制器(用于英特尔 CPU)设计时考虑了每通道 8 个 DIMM。这些高端计算机通常配备通道配置,需要至少四个 DIMM 才能使用它。

每个标准台式机内存模块支持 64 位数据总线,而基于服务器的内存模块每通道使用 72 位数据总线:额外的 8 位用于纠错码 (ECC)。由于增加了额外的性能和纠错特性的复杂性,R-DIMM 和 FB-DIMM 比台式机使用的内存贵得多。

CPU 和带宽增长

内存系统与 CPU 的演进有着直接的关系。随着更强大的处理器进入市场,需要更多的内存带宽来跟上 CPU 的处理速度。较慢的内存系统无法向快速的 CPU 提供足够的数据,这会导致处理器花费更多的时间在等待更多数据。内存系统的目标是在尽可能短的时间内为不同的任务存储和检索大量数据。如果没有同样快速的内存系统,CPU 将无法充分发挥效能导致效率低下。

下图说明了这一现象。摩尔定律描述 CPU 的处理能力每 18 个月翻一番。英特尔核心团队负责人 David “Dadi” Perlmutter 将其归类为“经济性和技术进步的简单关系”。计算机的效率在很大程度上依赖于内存系统来跟上 CPU 方面的改进。

单核 CPU 过去只是通过提高处理频率来提升性能。2004-2005 年,一种新的台式机 CPU 设计将处理器改进的动态从原始速度的单一因素转变为两个因素:处理速度以及核心数量。

此后的 CPU 性能提升依赖于核心频率的改进、处理器封装内的核心数量、具有更先进的预测和预取优化的缓存技术以及改进的总线效率以避免瓶颈。用于内存访问的数据预测和预取算法对处理器效率有重大影响。其他因素包括 L1、L2(和 L3)缓存的使用方式及其相关算法和访问分配。

第七章 EMI和内存控制器

由于内存速度从未真正能够跟上 CPU 性能,内存系统设计人员一直在使用多种手段改进性能。出于成本考虑原因,设计的主要目标一直是尽可能降低制造成本。

创新的设计人员没有选择提升内存的原始速度,而是通过并行访问的方法来提升整体速度。这些创造性的技术包括:双倍数据速率技术(DDR)、双通道或四通道模式(Dual or Quad Channel Modes)以及预取技术(Pre-fetching )等等。

需要特别主意的是,DDR DRAM 在技术工程角度存在成本限制,核心频率限制在 200MHz时整体成本最优。提升芯片外部总线传输频率相较于提升 DRAM 核心频率成本上要划算的多。
而为了达到上述目标,主板和内存模块必须在精确的信号管理和电磁干扰(EMI)控制机制方面取得设计和制造突破。因此,内存改进不仅仅是 DRAM 设计人员考虑的问题,也是从 CPU 架构师、芯片组和 DRAM 设计人员到主板和内存模块制造商的全行业协调努力的结果。

内存控制器

内存控制器的职责是对内存模块发送命令、管理内存模块和路由信号到内存模块。每个 DRAM 通常有四个或八个内存组,访问方式与 Microsoft Excel 工作表类似:有行和列。每当 CPU 需要读取或存储数据时,它都会通过内存控制器将信息发送到 RAM。

英特尔今年已经以 P35 和 X38 芯片组的形式发布了“Bearlake”3 系列家族下的第一代 DDR3 主板。Nvidia 和后来的 AMD 为其自己的 CPU 推出的未来芯片组也将在 2008 年支持 DDR3。当前的 DDR2/DDR3“组合”主板能够支持现有的 DDR2-800 或新的 DDR3-1066/1333 内存模块,但不能同时支持两个标准。DDR2 和 DDR3 使用不同的内存插槽,因为它们具有不同的模块键槽位置。

Intel 从Nehalem架构(45nm)开始整合了内存控制器(Integrated  Memory Controller,IMC)。现在市面上的Intel处理器都已经整合了IMC。

将内存集成到 CPU 封装中的直接经济效益是降低该特定平台的主板制造成本。技术效益是减短CPU 对内存传输数据路径。集成内存控制器消除了对物理前端总线 (FSB) 的需求。无需独立的内存控制器芯片,主板制造商的设计和测试过程变得更简单 : EMI 问题更少,整个系统本耗设更低。理论上,这种设计消除了前端总线(FSB)带来的带宽瓶颈,可以显著提高内存性能。

集成内存控制器的缺点是它占用了处理器芯片上的空间,而这些空间原本可以用于更大的 L1、L2 和 L3 缓存。此外,任何直接内存访问 (DMA) 都必须经由 CPU 才能进入内存,这会为需要快速访内存的其他组件(如显卡)带来额外的延迟。

讲一个好玩的,我入行的时候是在昆山的微兴做台式机主板,偶然发现 Intel 的内存兼容性要比 AMD 好多了。发现的原因是当前公司研发有一批DQA 人员负责测试。有时候项目到了后期,实在没有什么好测试的他们就会拿出来内存“扫”一圈。Intel 通常没啥问题,比如,上一次是两三个型号有问题,这一次仍然是这些有问题。但是 AMD 就麻烦了,这次扫出来的和上一次有问题的型号往往不同,让人无比头大。

后来稍微有点经验,对于有问题的内存通过 SPD 信息判断,然后升高电压来解决。万幸的是这种方法对于大部分问题都有效果。

不使用第三方库实现 IIS 麦克风转HTTP

这段代码展示了不使用第三方库实现在一个 HTTP Server, 然后电脑可以通过浏览器打开 http://ip/stream 即可听到 IIS 麦克风拾取到的声音。实验使用MSM261S4030H0的 IIS 音频传感器。

#include <WiFi.h>
#include <WebServer.h>
#include <driver/i2s.h>

// WiFi配置
const char* ssid = "YOURSSID";
const char* password = "YOURPASSWORD";

// I2S配置
#define I2S_MIC_WS  37
#define I2S_MIC_SD  48
#define I2S_MIC_SCK 45
#define SAMPLE_RATE 16000
#define BITS_PER_SAMPLE 32
#define BUFFER_SIZE 1024

WebServer server(80);
int32_t audioBuffer[BUFFER_SIZE];

void setup() {
  Serial.begin(2000000);
  
  // 初始化I2S
  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 8,
    .dma_buf_len = BUFFER_SIZE,
    .use_apll = false
  };
  
  i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_MIC_SCK,
    .ws_io_num = I2S_MIC_WS,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = I2S_MIC_SD
  };
  
  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_0, &pin_config);

  // 连接WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
  Serial.println("IP address: " + WiFi.localIP().toString());

  // 设置HTTP路由
  server.on("/stream", HTTP_GET, handleAudioStream);
  server.begin();
}

void loop() {
  server.handleClient();
  
  // 持续读取音频数据
  size_t bytesRead;
  i2s_read(I2S_NUM_0, &audioBuffer, BUFFER_SIZE * sizeof(int32_t), &bytesRead, portMAX_DELAY);
}

void handleAudioStream() {
  // 生成WAV文件头
  uint8_t wavHeader[44];
  generateWavHeader(wavHeader, SAMPLE_RATE, 32);
  
  // 发送HTTP头
  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, "audio/wav", "");
  
  // 先发送WAV头
  server.sendContent((const char*)wavHeader, sizeof(wavHeader));
  
  // 持续发送音频数据
  while(server.client().connected()) {
    size_t bytesRead;
    i2s_read(I2S_NUM_0, &audioBuffer, BUFFER_SIZE*4, &bytesRead, portMAX_DELAY);
    server.sendContent((const char*)audioBuffer, bytesRead);
  }
}


void generateWavHeader(uint8_t* header, uint32_t sampleRate, uint32_t bitDepth) {
    // RIFF块标识
    header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';
    
    // 文件总大小占位(后续更新)
    header[4] = 0; header[5] = 0; header[6] = 0; header[7] = 0;
    
    // WAVE格式标识
    header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E';
    
    // fmt子块标识
    header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' ';
    
    // fmt块大小(16字节)
    header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0;
    
    // 音频格式(1=PCM)
    header[20] = 1; header[21] = 0;
    
    // 声道数(1=单声道)
    header[22] = 1; header[23] = 0;
    
    // 采样率
    header[24] = sampleRate & 0xFF;
    header[25] = (sampleRate >> 8) & 0xFF;
    header[26] = (sampleRate >> 16) & 0xFF;
    header[27] = (sampleRate >> 24) & 0xFF;
    
    // 字节率 = 采样率 * 声道数 * 位深度/8
    uint32_t byteRate = sampleRate * (bitDepth/8);
    header[28] = byteRate & 0xFF;
    header[29] = (byteRate >> 8) & 0xFF;
    header[30] = (byteRate >> 16) & 0xFF;
    header[31] = (byteRate >> 24) & 0xFF;
    
    // 块对齐 = 声道数 * 位深度/8
    uint16_t blockAlign = (bitDepth/8);
    header[32] = blockAlign & 0xFF;
    header[33] = (blockAlign >> 8) & 0xFF;
    
    // 位深度
    header[34] = bitDepth & 0xFF;
    header[35] = (bitDepth >> 8) & 0xFF;
    
    // data子块标识
    header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a';
    
    // data块大小占位(后续更新)
    header[40] = 0; header[41] = 0; header[42] = 0; header[43] = 0;
}

类似的,有时候传感器送出 32Bits 数据,我们需要转为 16Bits输出,编写代码如下:

#include <WiFi.h>
#include <WebServer.h>
#include <driver/i2s.h>

// WiFi配置
const char* ssid = "CMCC-TSR6739";
const char* password = "!!1783az";

// I2S配置
#define I2S_MIC_WS  37
#define I2S_MIC_SD  48
#define I2S_MIC_SCK 45
#define SAMPLE_RATE 16000
#define BITS_PER_SAMPLE 32
#define BUFFER_SIZE 1024

WebServer server(80);
int32_t audioBuffer[BUFFER_SIZE];
int16_t webaudioBuffer[BUFFER_SIZE];

void setup() {
  Serial.begin(2000000);
  
  // 初始化I2S
  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 8,
    .dma_buf_len = BUFFER_SIZE,
    .use_apll = false
  };
  
  i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_MIC_SCK,
    .ws_io_num = I2S_MIC_WS,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = I2S_MIC_SD
  };
  
  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_0, &pin_config);

  // 连接WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
  Serial.println("IP address: " + WiFi.localIP().toString());

  // 设置HTTP路由
  server.on("/stream", HTTP_GET, handleAudioStream);
  server.begin();
}

void loop() {
  server.handleClient();
  
  // 持续读取音频数据
  size_t bytesRead;
  i2s_read(I2S_NUM_0, &audioBuffer, BUFFER_SIZE * sizeof(int32_t), &bytesRead, portMAX_DELAY);
}

void handleAudioStream() {
  // 生成WAV文件头
  uint8_t wavHeader[44];
  generateWavHeader(wavHeader, SAMPLE_RATE, 16);
  
  // 发送HTTP头
  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, "audio/wav", "");
  
  // 先发送WAV头
  server.sendContent((const char*)wavHeader, sizeof(wavHeader));
  
  // 持续发送音频数据
  while(server.client().connected()) {
    size_t bytesRead;
    i2s_read(I2S_NUM_0, &audioBuffer, BUFFER_SIZE*4, &bytesRead, portMAX_DELAY);
    for (int i=0;i<BUFFER_SIZE;i++) {
       int32_t audio_24bit = audioBuffer[i] >> 8;       // 提取高 24 位
       webaudioBuffer[i]=(int16_t)((audio_24bit + 128) >> 8);            // 四舍五入到 16 位
  
    }
    server.sendContent((const char*)webaudioBuffer, bytesRead/2);
  }
}


void generateWavHeader(uint8_t* header, uint32_t sampleRate, uint32_t bitDepth) {
    // RIFF块标识
    header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';
    
    // 文件总大小占位(后续更新)
    header[4] = 0; header[5] = 0; header[6] = 0; header[7] = 0;
    
    // WAVE格式标识
    header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E';
    
    // fmt子块标识
    header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' ';
    
    // fmt块大小(16字节)
    header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0;
    
    // 音频格式(1=PCM)
    header[20] = 1; header[21] = 0;
    
    // 声道数(1=单声道)
    header[22] = 1; header[23] = 0;
    
    // 采样率
    header[24] = sampleRate & 0xFF;
    header[25] = (sampleRate >> 8) & 0xFF;
    header[26] = (sampleRate >> 16) & 0xFF;
    header[27] = (sampleRate >> 24) & 0xFF;
    
    // 字节率 = 采样率 * 声道数 * 位深度/8
    uint32_t byteRate = sampleRate * (bitDepth/8);
    header[28] = byteRate & 0xFF;
    header[29] = (byteRate >> 8) & 0xFF;
    header[30] = (byteRate >> 16) & 0xFF;
    header[31] = (byteRate >> 24) & 0xFF;
    
    // 块对齐 = 声道数 * 位深度/8
    uint16_t blockAlign = (bitDepth/8);
    header[32] = blockAlign & 0xFF;
    header[33] = (blockAlign >> 8) & 0xFF;
    
    // 位深度
    header[34] = bitDepth & 0xFF;
    header[35] = (bitDepth >> 8) & 0xFF;
    
    // data子块标识
    header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a';
    
    // data块大小占位(后续更新)
    header[40] = 0; header[41] = 0; header[42] = 0; header[43] = 0;
}

Easy X用点画阿基米德螺旋线

#include <graphics.h>  // EasyX图形库头文件
#include <conio.h>     // 用于_getch()
#include <math.h>    

#define a 0 // 从原点开始
#define b 60*3.1415/180 //半径增加的速率

int main()
{
    int x,y;
    // 初始化640x480像素的图形窗口
    initgraph(640, 480);

    for (int i = 0; i < 360*30; i++) {
        x = 320 + (a+  b * (i * 3.1415 / 180)) * cos(i * 3.1415 / 180);
        y=  240+  (a + b * (i * 3.1415 / 180)) * sin(i * 3.1415 / 180);
        putpixel(x, y, YELLOW);  
    }

    // 保持窗口显示
    _getch();

    // 关闭图形窗口
    closegraph();
    return 0;
}

运行结果:

一键USB通断器

在一些特别情况下,我们会碰到 USB 兼容性问题。比如,我正在使用的 DELL C2722DE 显示器,通过 TypeC 连接主机,除了能够显示之外,还内置了 USB Hub ,但是它和我的微软鼠标存在兼容性问题,如果插着显示器休眠,或者开机的话,进入 Windows之后鼠标是无法直接使用,必须插拔一次才能正常工作。

针对这种情况,这次设计了一个 USB 插拔装置,遇到问题的时候无需做插拔动作,而是通过按键一次自行完成一个插拔的模拟,这样可以方便使用。USB插拔动作可以分解为2步,插入时是先接通5V供电,然后接通D+/D-信号。如果仔细观察USB接头,可以发现USB D+/D- 引脚会比5V和GND 短一点,这样就能保证插入的时候是先接通供电,然后再接入信号的。这样可以避免插入时信号先于供电接通,电流直接倒灌到芯片中。类似的,拔出时,是先断开信号,然后再切断供电。

硬件方面使用了3个芯片:CH554 /CH442/SY6280AAC。CH554是一个单片机,这里作用是获得按钮状态,然后根据状态控制USB信号的切换和USB母头上的5V输出; CH442进行信号切换, SY6280AAC 是功率电子开关芯片,这里我们用用它控制USB供电输出。

CH442是一款低阻宽带双向模拟开关芯片,包含2路单刀双掷二选一开关。这里用作 USB 信号二选一。

CH442

PCB 设计如下:

焊接后的成品,刚好能装在外壳中:

CH554 外围只需要1个10K电阻,2个0.1uf 电容即可让它工作起来。然后通过P1 Header 4 下载数据非常方便。此外,CH_IN 适用于控制CH442信号切换,CTRL1用于控制SY6280AAC。

代码部分非常简单:

#define CH_IN 14
#define LED   15
#define CTRL1 16
#define KEY   17
void setup() {
  pinMode(CH_IN,OUTPUT);
  pinMode(LED,OUTPUT);
  pinMode(CTRL1,OUTPUT);
  pinMode(KEY,INPUT_PULLUP);
    
  digitalWrite(CH_IN,LOW); // Switch to 1
  digitalWrite(LED,LOW); // 不亮
  digitalWrite(CTRL1,HIGH); // 供电    
}
unsigned long int Elsp=0;

void loop() {
  if (digitalRead(KEY)==LOW) {
      digitalWrite(LED,HIGH); // 点亮 LED
      digitalWrite(CH_IN,HIGH); // 断开数据线
      delay(100);
      digitalWrite(CTRL1,LOW); // 断开USB供电
      Elsp=millis();
  } else {
      // Key 抬起
      if ((Elsp!=0)&&(millis()-Elsp>500)) {
          Elsp=0;
          digitalWrite(LED,LOW); // 关闭 LED          
          digitalWrite(CTRL1,HIGH); // 开始USB供电
          delay(100);
          digitalWrite(CH_IN,LOW); // 接通数据线
        }
    }
}

适用范围:如果你遇到的问题通过插拔一次可以解决,并且出现问题的时候USB端口能够正常供电,那么可以尝试本次的设计。运气好的话,因为引入了切换元件使得USB信号发生变化,每次都可以直接用;稍微差一些的话,出现问题的时候按下按钮即可模拟插拔让设备工作起来。

制作一个PCIE设备的空驱动

一些情况下我们需要空驱动来避免设备管理器中出现 Yellow Bang(当然,这种驱动没有任何功能)。这个问题我请教了一下天杀,在他的指导下写了这篇问斩给。

本文介绍如何制作一个 PCIE 的空驱动。

本文提到的软件包括 OpenSSL (来自 https://slproweb.com/products/Win32OpenSSL.html的Win64OpenSSL_Light-3_5_0.exe),此外,签名使用到的文件都来自 Windows SDK 和 WDK。

一.准备用于签名的密钥文件

1. 生成私钥‌:openssl genrsa -out LABZprivate.key 2048

运行结果:

2. ‌创建证书签名请求(CSR‌:

openssl req -new -key LABZprivate.key -out LABZrequest.csr

运行结果:

3.生成自签名证书(.crt)文件

openssl x509 -req -days 365 -in LABZrequest.csr -signkey LABZprivate.key -out LABZcertificate.crt

运行结果:

4.转换为 pfx 格式

openssl pkcs12 -export -out LABZPFX.pfx -inkey LABZprivate.key -in LABZcertificate.crt

会要求你设定一个密码,这里使用 “labz123”

运行结果:

5. 生成 cer 文件

Openssl pkcs12 -in LABZPFX.pfx -clcerts -out LABZcert.cer -nodes

这里输入前面设定的密码 labz123

至此,需要用到的签名的密钥文件已经准备好了。

此外,还可以不用openssl来生成证书,WDK提供了一个更简单的生成自签名证书的工具:makecert.exe。有兴趣的朋友可以继续研究。

二.修改 INF文件

以如下设备为例,查看属性

将上述信息写入EmptyPciDriver.inf

三.使用工具生成 CAT文件以及签名

1.生成 CAT 文件(可以看作是 INF 的签名文件)

“C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\inf2cat.exe” /driver:”C:\ndrv ” /os:10_NI_X64

2.生成 INF 的 Hash

“C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe” sign /f LABZPFX.pfx /p labz123 /fd sha256 “c:\ndrv\EmptyPciDriver.cat”

四.测试

1.将前面的公钥加入目标机中(只用于测试,不要用于工作机)

Certutil  -addstore root LABZcert.cer

2.将公钥加入受信任的发布者列表中

Certutil -addstore TrustedPublisher LABZcert.cer

然后像普通驱动一样安装即可:

安装后设备管理器中可以看到:

测试完成之后,拿去做正式签名,之后对于相同 VID DID SVID SDID的PCIE设备就可以直接使用了(无需步骤四提到在目标机中导入公钥的步骤)

本文提到的 INF 文件下载:

本文生成的签名文件:

ImageMagick VC 代码的一些细节

大多数时候命令行足够用了,但是对于一些无法写入同一条命令行的组合操作,如果直接使用 VC 编程可以大大提升效率。

        // 读取文件
        image.read(filename);

        // 改变大小
        image.resize(Geometry(image.columns() * resize/100, image.rows() *resize / 100));
        // 设置背景颜色用于填充
        image.backgroundColor(Color("Yellow"));
        //旋转
        image.rotate(angle);
        // 特别主意,如果没有下面这个 repage 会导致计算坐标有问题
        image.repage();
        //以中心为原点裁剪图片
        image.crop(Geometry(1920, 1080, (image.columns()/2 - 1920/2), (image.rows()/2 - 1080/2)));

        // 保存图片
        image.write(Output);

参考:

1.https://zhuanlan.zhihu.com/p/606327058 (亲测可用)

C# 编写的UEFI迷宫游戏

这次介绍的迷宫游戏是C#编写的3D游戏。基于前面介绍的bflat编译完成。项目地址在 https://github.com/MichalStrehovsky/uefimaze/tree/master

下载好代码之后就可以进行编译(特别需要注意的是,需要在所在的 src目录下进行编译。如果你没有将 bflat放入path中,那么需要给出它的绝对路径):

bflat build --os:uefi --stdlib:zero -o:bootx64.efi  

上下左右四个按键移动视角。编译之后生成bootx64.efi。下面是在 VirtualBox上测试的视频:

源代码:

编译后的代码:

在 Arduino Serial Plotter 上绘制正弦波的例子


void setup() {
  Serial.begin(9600);  // 初始化串口通信
}

void loop() {
  static float angle = 0;  // 角度变量
  static const float step = 0.1;  // 角度增量
  
  // 计算正弦值(范围0-1)
  float sinValue = (sin(angle) + 1) / 2;  // 调整到0-1范围
  
  // 输出到串口绘图仪
  Serial.println(sinValue * 100);  // 放大到0-100范围
  
  angle += step;  // 增加角度
  if(angle >= 2*PI) angle = 0;  // 重置角度
  
  delay(50);  // 控制波形刷新速度
}

运行结果

我的故乡:大庆市

当我写下标题的时候,忽然有一种30年前坐在教室里完成老师作文题目的感觉。相比散装的江苏省,东北一直是“整装”的。这句话的意思是,对于江苏人来说他们永远都会介绍自己是苏州人或者南京人,很少会碰见自称江苏人。但是对于东北人来说,无论黑龙江吉林或者辽宁人,通常都会自称:“东北人”。

我在大庆生活到了18岁,然后离开了这个城市去成都求学读书。相当于从祖国的最东走到大西南。大学四年一直是火车来回,当时并没有直达列车需要在北京中转。从大庆到北京,最快18小时;然后再坐27小时50分的车从北京到成都。好在路上总是有同学相伴,加之年轻并不会觉得无聊。三天的路程也只是弹指一挥间。

大庆是一座在荒原上平地而起的城市,它名称的来源是 1959年恰逢建国十周年,发现的大庆油田。等到了1979年,更名为大庆市。也因为它的“年轻”,以至于很多大庆子弟离开之后到其他地方都会觉得为什么当地的楼房那么旧。

我住的地方叫做“让胡路区”,关于这个名称的来源有一些说法:

1954年建滨洲铁路时设让胡路站,区以站名;又说因附近水泡子形状像芽葫芦(轧葫芦、亚葫芦)得名;轧葫芦泡西北端跑县城百里,南北斜长约十数里,两端水阔中间狭细如轧葫芦,故名;相传早年有一家山东人迁移到此,因水泡子其开头很象关里家的轧葫芦,故名轧葫芦泡;1954面简化为让胡路。【参考1】

大庆市的出名是因为“大庆油田”,从历史上来说,大庆市是先有的企业(油田),然后才有的市政府。在我小时候,那个只有“座机”的时代,这样的割裂非常明显。那时候大庆有2种座机号码,5开头的管理局号码(当年我家就是管理局号,现在我还记得很清楚“553455”)以及6开头的市政号码。两种号码经常无法互通,需要拨号很多次才能侥幸打通。如果想成功直播,那就只能在前面加上区号:0459。只是这样做的话,是按照长途收费的。

这是一座名副其实的油城,根据baidu百科:“截至2022年10月,中国陆上最大油田大庆油田被发现63年来,已累计生产原油逾24亿吨”。相当于全国人民2吨油。我小时候经常听到的说法是一顿原油的价格比不上一吨的酱油。所有的物资都是在国家的统一调配下进行的。

对于当年日本侵华战争时为什么没有发现大庆油田,大约有两种说法:

说法一:日本勘探技术的问题以及抗日武装的功劳

当时,日本有一个机构,叫满洲炭矿株式会社,早在1926年,它就专门跑到中国东北来找石油。到了1928年,日本人在中国东北寻找石油的心情愈加迫切。时任满铁地质调查所参事的日本地质专家新带国太郎就曾在1929年春天带着人,沿东清铁路到牡丹江上游的森林地带寻找石油,但经过两个多月的探察却一无所获。不死心的他在1930年4月又在满洲里的扎赉诺尔煤矿进行过第二次勘探,再次失败。不止这一路人马。为了在东北找到石油,日本当时投入了大量人力物力,也用尽了当时他们所掌握的一切勘探和采矿设备,但始终没有发现一滴油。直到1941年“太平洋战争”爆发,所有日本在中国勘探石油的队伍都被征调到了东南亚,参与那里的油田恢复和开采(英军和荷军在撤离时摧毁了油田),“在中国东北找到石油”这个目标最终被放弃。

是日本人勘探能力和技术水平不行吗?也不完全是,现在回过头来看,其实当时还是很凶险的。根据旅日学者徐静波的回忆,2010年,他和一位日本客人拜访时任大庆市副市长的栾莹,栾莹曾告诉他,当时日本人打的其中一口井,离后来喷油的“松基三井”只有2公里远——当时日本人打的井深度是1000米,而“松基三井”是打到1300米深时喷油的。就差300米。而根据黑龙江省安达市档案局局长、安达市志办主任李生的回忆,大庆油田的主力产油区有一口井叫“萨一井”,这口井当年下钻到680米的时候就遇到了油层。而这口井与当年日本人打的井距离更近——相距仅1公里。如果当时日本人把打井的方位再多挪一公里,或者多打几口井,那么就很有可能率先发现大庆油田。日本人当初之所以没发现大庆油田,除了所谓的“运气”和“技术”之外,按照李生的说法,还和当年共产党领导的东北抗联和地下抗日武装有很大关系。

在打出“萨一井”的那个杏树岗,当年就是当地抗日游击队的大本营。在频繁的骚扰和袭击下,日本人其实很难大张旗鼓地拉开架势打井钻油,换句话说,其实当年日本人也并非像我们想象的那样,能大摇大摆地在中国东北随意勘探。【参考2】

说法二:毗邻大庆市的安达市是当时的日本细菌战试验场【参考3】,日本鬼子自然不愿意认真进行勘探。

相比之下,第一种说法更令人信服。所谓的“国运”更是一代又一代人奋力托举出来的结果。

从我有记事起,大庆的生活条件并不好,主要是物资短缺。因为冬季的寒冷,能够吃到的蔬菜品种非常少,主要就是萝卜白菜土豆这样耐寒的贮存菜。秋天的时候,单位会“分菜”。通常就是企业(比如,我父亲的单位是勘探开发设计院),从地方购买白菜,然后按照人头分下去。早些年住在平房,有些家庭会有菜窖。这是高寒地区人民群众多年经验总结出来的经验。在地上挖出来一个坑,然后留个门,秋天的时候将白菜萝卜贮藏在其中,这样即便外面零下三十度,地窖种仍然能够维持一定温度使得蔬菜不至于被冻坏。有需要的时候打开门,顺着梯子下去取出来就可以吃到。此外,还有酸菜,是秋天的时候在家中准备巨大的缸,然后一层层的码上白菜最上面放上一块大石头放置在家中。温度不能太高也不能太低,在事宜的温度中,白菜会发酵,然后成为整个冬季的食物。告诉你这些的人不会告诉你在发酵的过程中白菜会发出异味,也不会告诉你,在这个过程中夜深人静的时候缸中微生物会产生气体,因此,这个庞然大物对于一个少年是恐怖的存在。当然,酸菜炒粉条和酸菜炒肉是非常下饭的存在,有机会的朋友可以尝试一下。

为了让我接收白菜/酸菜,我母亲告诉我白菜清热解毒,人一百天不吃白菜就会中毒,这种说法让我深信不疑。现在虽然我不在家乡,有机会还是很爱吃白菜蘸酱的。除了白菜,给我留下悲伤记忆的更多的就是萝卜。印象中我的幼儿园时光就是充斥着萝卜的味道。当地的一种做法是:将萝卜过水,让它变得松软,然后挤出水分,蘸着大酱吃。味道尚可接受,唯独萝卜过水产生的味道让人难忘…….

至于土豆,倒是没有多少悲伤的感觉,可能是这种东西做出来的土豆泥还有土豆炖粉条多多少少会有肉的存在,味道并不差吧。东北的土豆大约是沙地的缘故,吃起来有着香甜的感觉。土豆炖粉条配上辣椒油,让人瞬间有着温暖的感觉。

从我小时候起,一直听到的“菜篮子工程”(搜了一下,英文名“shopping basket program” 是农业部从1988年开始一直坚持进行的项目【参考4】,至今已经快四十年),随着这个项目的进行,冬天可以选择的蔬菜品种也越来越多,西红柿韭菜大头菜等等。

冬天的水果品种也非常少,印象中只有桔子苹果。我年少时,这些也主要靠单位“分”,市面上买不到。比较有名的苹果有“国光”“红富士”,我比较偏向口感清爽类型的,因此不太喜欢“黄元帅”这种面甜的。当时印象深刻的还有有时候单位会分品相很差的“爱国苹果”。应该是一些地方受灾,苹果没有卖相,然后国有企业承担社会责任,购买之后作为福利下发而来。

大庆本地没有苹果,本地的水果是被成为“沙果”的水果,个头较小,吃起来酸甜口。我不太爱吃,因为太小,没吃几口就没了。

偶然单位能发一些香蕉,这对于我们来说就是少见的热带水果。只是每次拿到的都是真正的“蕉绿”。等我走出家乡,再也没有看到过如此之绿的香蕉。大约是因为贮藏不易的原因,因此发下来的都是翠绿颜色的。到家之后,父亲会含住白酒喷洒在上面,然后用塑料袋仔细的包装起来,放在床下,然后全家人静待成熟,而我总是迫不及待的每隔几天就拉出来看看。

若干年后,我偶然间看到关于北京一所中学的故事,说这家中学以前生源一直是达官显贵,后来被人批判不得不向平民开放。为了筛选,学校搞了一个骚操作,选拔的时候只是面试,拿出热带水果的画册,比如:芒果,然后让学生辨认…….普通人哪怕有看报纸的机会看到的也都是黑白照片,自然不认识热带水果,而能认得此物的通常也都是富贵人家的子弟了。由此,最终招上来的仍然是家世显赫的子弟,

参考:

1.http://www.tcmap.com.cn/heilongjiang/ranghuluqu.html

2.https://zhuanlan.zhihu.com/p/138814640

3.https://baike.baidu.com/item/%E4%BE%B5%E5%8D%8E%E6%97%A5%E5%86%9B%E7%AC%AC%E4%B8%83%E4%B8%89%E4%B8%80%E7%BB%86%E8%8F%8C%E9%83%A8%E9%98%9F%E5%AE%89%E8%BE%BE%E7%89%B9%E5%88%AB%E5%AE%9E%E9%AA%8C%E5%9C%BA%E9%81%97%E5%9D%80/61965399

4.https://baike.baidu.com/item/%E8%8F%9C%E7%AF%AE%E5%AD%90%E5%B7%A5%E7%A8%8B/7105237