记事本.LOG 的秘密

Windows 自带的记事本(Notepad)是一个非常方便易用的文本编辑工具。虽然我从Window3.1开就使用这个软件,但是最近刚知道它有一个特别的功能:如果在文本开头写入  .LOG ,那么保存之后每次使用记事本打开这个文件都会自动添加当前的日期和时间。

这个功能是记事本代码实现的,在https://github.com/vxiiduu/NotepadEx 这个项目上可以看到(这个项目代码来自泄露的 Windows XP 代码)。在其中的 npfile.c 文件中可以看到检测 “.LOG”字样的代码:

       // null terminate it.  Safe even if nChars==0 because it is 1 TCHAR bigger
       *(lpch+nChars)= (TCHAR) 0;
   
       // Set 'fLog' if first characters in file are ".LOG"
       fLog= *lpch++ == TEXT('.') && *lpch++ == TEXT('L') &&
             *lpch++ == TEXT('O') && *lpch == TEXT('G');

之后,同样文件还有根据标志位进行动作的代码:

    /* If file starts with ".LOG" go to end and stamp date time */
    if (fLog)
    {
       SendMessage( hwndEdit, EM_SETSEL, (WPARAM)nChars, (LPARAM)nChars);
       SendMessage( hwndEdit, EM_SCROLLCARET, 0, 0);
       InsertDateTime(TRUE);
}

为了验证猜想,我对上述代码进行修改,判断条件之后除了增加时间再增加一段我自定义的字符串:

    /* If file starts with ".LOG" go to end and stamp date time */
    if (fLog)
    {
       SendMessage( hwndEdit, EM_SETSEL, (WPARAM)nChars, (LPARAM)nChars);
       SendMessage( hwndEdit, EM_SCROLLCARET, 0, 0);
       InsertDateTime(TRUE);
	   //LABZ_Debug_Start
	   TCHAR szMsg[] = TEXT("www.lab-z.com");
	   SendMessage(hwndEdit, EM_REPLACESEL, TRUE, (LPARAM)szMsg);
	   //LABZ_Debug_End
}

编译之后用生成的 EXE 打开 TEXT 文件,可以看到除了时间,还增加了我自定义的代码。

未修改的完整项目可以在这类下载(就是 Github 上的源代码):

工作的视频

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

BMX60 旋转一只兔子

一直以来我想通过传感器让现实世界中的物体控制电脑中的物体姿态,最近入手了DFRobot的BMX160九轴加速度传感器模块【参考1】。

所谓9轴指的是三个加速度轴,三个地测轴和三个陀螺仪轴。简单的介绍一下需要了解的物理知识:首先,如果物体发生运动,那么在三个轴方向会出现加速度,那么我们可以知道它运动的指向,此外比较特别的是因为重力影响,所以一直会存在一个加速度G;接下来因为地磁的存在(指南针的基本原理),我们可以通过地磁轴得到相对于地磁的方向;最后陀螺因为惯性,它会一直保持指向,因此陀螺仪的轴可以用来反映当前时间和上一个时间姿态区别,得到诸如角速度这种方向。

很明显,如果单纯得到上面的9个参数,我们仍然难以用其中的一组描述物体当前的状态,因此,需要经过算法处理得到当前的状态。这个过程叫做“姿态解算”,同样也叫做姿态分析、姿态估计、姿态融合。姿态解算是根据IMU(惯性测量单元)数据(陀螺仪、加速度计、地磁计等)求解出飞行器的空中姿态,所以也叫做IMU数据融合。【参考2】

因为本人能力有限,无力应对复杂的数学,因此这里通过硬件和现成的库来解决。

DFRobot的BMX160引脚定义如下:

丝印功能描述
VCC电源正极
GND电源负极
SCLI2C时钟线
SDAI2C数据线
CSBBMX160协议选择引脚
INIT1BMX160外部中断1
INIT2BMX160外部中断2
ADDRI2C地址选择

需要特别注意的是 CSB 是一个 SPI 和 I2C 选择引脚,这次我们使用I2C, 因此只需要链接 VCC GND  SCL 和 SDA 引脚即可。我是用的是 ESP32 FireBeetle 进行测试,

连接好之后推荐使用 DFRobot 的BMX160库先进行测试【参考4】,确保串口能看到输出。

硬件部分非常简单,接下来要进行软件方面的编写。同样,还需要了解一些关于姿态描述的基本知识。描述姿态的主要方法有两种:欧拉角和四元数。下图是欧拉角的直观描述。三个参数分别是航向角、俯仰角和横滚角。

需要注意的是对于欧拉角有两种:动态和静态欧拉角。动态欧拉角指的是轴跟着物体姿态旋转,比如,下图中物体圆转之后蓝色XYZ坐标系发生了变化,变成了红色的XYZ,如果再次旋转,那么航向角、俯仰角和横滚角都是要相对于红色XYZ的。静态欧拉角的话,就是一直使用蓝色 XYZ。从资料上看,通常我们提到的欧拉角都是动态欧拉角。但是很多三维设计软件使用的是静态欧拉角。

欧拉角非常直观容易理解。但是在使用中,这种描述方法会存在“万向节死锁(gimbal lock)”的问题。就是说在一些特别的角度,两个轴重合了,然后再也不会分开,继续旋转时欧拉角只有2个参数会变。B站有很多视频讲述这个问题有兴趣的朋友可以看看。

为了解决这个问题,引入了四元数的概念。四元数没有欧拉角那么直观,也非常难以理解。

这次的目标是使用 Adafruit 提供的库和网站来直观的实现旋转一只兔子。

首先安装 BMX160 和 Adafruit_Sensor库;之后,烧录下面的程序:

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Mahony_BMX160.h>
#include <Madgwick_BMX160.h>
#include <DPEng_BMX160.h>


// Create sensor instance.
DPEng_BMX160 dpEng = DPEng_BMX160(0x160A, 0x160B, 0x160C);

// Mag calibration values are calculated via ahrs_calibration example sketch results.
// These values must be determined for each baord/environment.
// See the image in this sketch folder for the values used
// below.

// Offsets applied to raw x/y/z mag values
float mag_offsets[3]            = { 9.83F, 4.42F, -6.97F };

// Soft iron error compensation matrix
float mag_softiron_matrix[3][3] = { {  0.586,  0.006,  0.001 },
                                    {  0.006,  0.601, -0.002 },
                                    {  0.001,  -0.002,  2.835 } };

float mag_field_strength        = 56.33F;

// Offsets applied to compensate for gyro zero-drift error for x/y/z
float gyro_zero_offsets[3]      = { 0.0F, 0.0F, 0.0F };

// Mahony is lighter weight as a filter and should be used
// on slower systems
Mahony_BMX160 filter;
//Madgwick_BMX160 filter;

void setup()
{
  Serial.begin(115200);

  // Wait for the Serial Monitor to open (comment out to run without Serial Monitor)
  // while(!Serial);

  Serial.println(F("DPEng AHRS Fusion Example")); Serial.println("");

  // Initialize the sensors.
  if(!dpEng.begin(BMX160_ACCELRANGE_4G, GYRO_RANGE_250DPS))
  {
    /* There was a problem detecting the BMX160 ... check your connections */
    Serial.println("Ooops, no BMX160 detected ... Check your wiring!");
    while(1);
  }
  
  filter.begin();
}

void loop(void)
{
  sensors_event_t accel_event;
  sensors_event_t gyro_event;
  sensors_event_t mag_event;

  // Get new data samples
  dpEng.getEvent(&accel_event, &gyro_event, &mag_event);

  // Apply mag offset compensation (base values in uTesla)
  float x = mag_event.magnetic.x - mag_offsets[0];
  float y = mag_event.magnetic.y - mag_offsets[1];
  float z = mag_event.magnetic.z - mag_offsets[2];

  // Apply mag soft iron error compensation
  float mx = x * mag_softiron_matrix[0][0] + y * mag_softiron_matrix[0][1] + z * mag_softiron_matrix[0][2];
  float my = x * mag_softiron_matrix[1][0] + y * mag_softiron_matrix[1][1] + z * mag_softiron_matrix[1][2];
  float mz = x * mag_softiron_matrix[2][0] + y * mag_softiron_matrix[2][1] + z * mag_softiron_matrix[2][2];

  // Apply gyro zero-rate error compensation
  float gx = gyro_event.gyro.x + gyro_zero_offsets[0];
  float gy = gyro_event.gyro.y + gyro_zero_offsets[1];
  float gz = gyro_event.gyro.z + gyro_zero_offsets[2];

  // Update the filter
  filter.update(gx, gy, gz,
                accel_event.acceleration.x, accel_event.acceleration.y, accel_event.acceleration.z,
                mx, my, mz,
				mag_event.timestamp);

  // Print the orientation filter output
  // Note: To avoid gimbal lock you should read quaternions not Euler
  // angles, but Euler angles are used here since they are easier to
  // understand looking at the raw values. See the ble fusion sketch for
  // and example of working with quaternion data.
  float roll = filter.getRoll();
  float pitch = filter.getPitch();
  float heading = filter.getYaw();
  //Serial.print(millis());
  Serial.print("Orientation: ");
  Serial.print(heading);
  Serial.print(",");
  Serial.print(pitch);
  Serial.print(",");
  Serial.println(roll);

  delay(100);
}

在串口管理器中看到输出表明工作正常之后就可以进行下一步。

我们需要网页读取串口数据,在 Chrome 上地址栏输入 chrome://flags,打开 Experimental Web Platform features:

之后,打开 https://adafruit.github.io/Adafruit_WebSerial_3DModelViewer/ 页面,设置波特率为 115200 , 同时选择“Euler Angles”:

接下来选择使用的串口(记得要关闭 Arduino 的串口,否则无法打开)

然后你就可以随心所欲的旋转,屏幕上的兔子会跟着你的板卡转动。

参考:

  1. https://www.dfrobot.com.cn/goods-2947.html
  2. https://www.cnblogs.com/codeit/p/15832606.html
  3. https://wiki.dfrobot.com.cn/_SKU_SEN0373_BMX160_9-axis_sensor_module
  4. https://github.com/DFRobot/DFRobot_BMX160
  5. https://learn.adafruit.com/adafruit-bno055-absolute-orientation-sensor/webserial-visualizer

LattePanda 主板更换开机Logo 方法

LattePand使用 AMI BIOS , 使用官方工具可以方便的更换开机 Logo。

第一步:从官方网站上下载IFWI 文件(BIOS+CSME, 16MB)。接下来的操作都是基于这个版本进行的;

第二步,使用AMI Change Logo工具,Load Image 选中上面提到的IFWI 文件,然后通过Save Logo 保存内置的Logo。

保存后的 Logo是 BMP 格式。

查看属性这是一个800X400,24Bits的BMP 图片,通常情况下,BIOS能够支持 BMP和 JPEG 格式,但是保险起见,最好使用和之前的Logo相同的格式。例如,这里我们就更换图片为双鸭山大学的Logo,分辨率相同,同样是24Bit BMP格式。

之后继续使用Change Logo 软件,使用Browse 选择上面这个图片。最终使用 Save Image As 保存为另外一个文件就制作好了替换Logo的IFWI。

第三步,我们使用FPTW 进行刷新,同样这个是LattePanda官方提供的软件。在管理员模式下的 cmd 窗口使用fptw -f newlogo.bin 即可刷写​:

​特别注意:刷写 IFWI 有风险,最好提前准备 ​SPINOR 烧录器,如果烧写失败随时可以挽救回来

常用测试工具以及软件下载

最近购买了一个网盘,可以直接下载。如果直接点击无法打开,请拷贝链接然后在新窗口中打开。

HE_HE_v1.25.03.16_Full 好用的硬件访问读取工具,完全版 (186.38M)

HE_HE_v1.25.03.16_Lite 好用的硬件访问读取工具,标准版 (4.73MB)

AutoWim 一款帮助你安装 Wim 文件的 WinPE

WinPELabz.zip 用于全盘备份的工具

LattePanda 官方资料 来自 GitHub 的LattePanda 资料 (2024年4月30日)

FileZilla_3.66.5 一个 FTP 工具

Potplayer 一个视频播放软件,可以用于多种视频格式的播放

vokoscreenNG-4.0.0-win64 录制本地屏幕内容为视频的工具

合金弹头1-7模拟器 用于性能评估的游戏

DiskGenius 5.6.1 硬盘分区工具

ffmpeg-2025-03-06-git-696ea1c223-essentials_build.7z FFmpeg 工具

RaptorLake-P UART Shell 测试工具

这个工具是用来给 RaptorLake-P Shell 下进行 Uart 测试的工具,它通过 PCH UART输出字符。

在使用之前,请保证:

  1. PCH 对应的 UART 已经 Enable, 在 Shell 下能够看到对应的PCI Controller;
  2. UART 对应的 GPIO 已经设置为 Native UART 功能。

使用方法:

zu4r &lt;UART 编号>

例如, zu4r 0 将会从第一个 PCH UART对应的引脚,以 115200 波特率输出 www.lab-z.com。

程序在这里下载(无源代码)

Ch32x033 Arduino 环境USB 键盘开发

目前已经有 Ch32x035 的 Arduino 开发环境,在 https://github.com/openwch/arduino_core_ch32 可以看到。美中不足的是这套环境中没有提供 USB 的支持。经过研究可以在代码中加入官方示例代码来实现 USB 功能。这次演示的是在 Ch32X033 板子上实现USB 键盘每隔一段时间输入字符的功能。

第一步,按照上面提到的方法安装 ch32x035的Arduino 支持;

第二步,编写代码。这里参考了Ch55xduino 的方法,创建了 src 目录,然后在这个目录中再创建userUsbKB 目录,对于 USB 支持的代码都在其中。基本上相当于将 WCH 官方例子文件都放置在此。

最后,编写Arduino 代码。基本想法是:将按键数据放置在 Buffer 中,然后使用USBFS_Endp_DataUp() 函数即可发送出去。

#include "src\\userUsbKB\\usbdKBMS.h"

uint8_t  Buffer[ 8 ];
long int Elsp;
boolean Flag=true;

void setup() {
  Serial2.begin(115200);
  delay(500);
  /* Usb Init */
  USBFS_RCC_Init();
  USBFS_Device_Init( ENABLE , PWR_VDD_SupplyVoltage( ));
  USB_Sleep_Wakeup_CFG( );
  Elsp=millis();
}



void loop() {
  /* Determine if enumeration is complete, perform data transfer if completed */
//Serial.println(millis());
  if ( USBFS_DevEnumStatus )
  {
    //Serial.println(millis());
    /* Handle keyboard scan data */
    KB_Scan_Handle(  );

    /* Handle keyboard lighting */
    KB_LED_Handle( );

    /* Handle mouse scan data */
    MS_Scan_Handle( );

    /* Handle USART2 receiving data */
    USART2_Receive_Handle( );
    if ((millis()-Elsp>5000)&&(Flag)) {
        Buffer[2]=0x0F; //"L"
        Buffer[3]=0x04; //"A"
        Buffer[4]=0x05; //"B"
        Buffer[5]=0x2D; //"-"
        Buffer[6]=0x1D; //"Z"
        
        USBFS_Endp_DataUp( DEF_UEP1, Buffer, sizeof( Buffer ), DEF_UEP_CPY_LOAD );
        memset(Buffer,0,sizeof(Buffer));
        Serial2.println("Send press");
        Flag=false;
      } 
    if (millis()-Elsp>5010) {
        USBFS_Endp_DataUp( DEF_UEP1, Buffer, sizeof( Buffer ), DEF_UEP_CPY_LOAD );
        Serial2.println("Send Release");
        Elsp=millis();
        Flag=true;
      } 

  }
}

这个只是一个简单的Demo 还并不完善,最好的状态是类似 Arduino Leonardo ,用面向对象的方法将所需要的完整封装起来这样才更便于使用。

LattePanda Mu X86计算模块套件

最近入手了LattePanda Mu ,这是一款微型 x86 计算模块,模块上带有 Intel N100 四核处理器、8GB LPDDR5 内存和 64GB 存储。搭配基础载板,后可以扩展出来
2个 USB 3.2 10Gbps 接口,1个千兆以太网,2个USB 2.0 ,1个HDMI 2.0,1个PCIEx1 接口。

供电方面可以通过USB Type-C(仅供电) 和12V DC 5.5×2.5mm 进行。

全部配件
全部配件

完整的系统分为2部分:核心板和载板。同样的核心板可以搭配不同的载板实现更强的扩展。

核心板非常迷你,接近信用卡尺寸:

核心板

左上角的芯片是 LPDDR5 内存芯片,下面是 Super Io 芯片,中间是 Intel N100 芯片。

核心板正面
核心板背面
基础载板
完整安装

堆叠安装非常简单,在核心板上安装好风扇后即可插入载板中。

使用载板上的 HDMI 连接显示器

内置的 EMMC 已经预置了 Windows 11,连接好之后就可以开机上电。可以看出我使用笔记本的 TypeC 进行供电。

使用上非常简单,和普通的 X86 没有差别。

下面这个视频测试的是播放视频

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

这个视频是模拟器运行合金弹头

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

这款板子在设计上更倾向于开发板,后面会基于这个开发板来完成一些有趣的设计和研究。

C语言内联函数

在阅读代码的过程中,我们有时候会碰到使用 inline 修饰的函数。这是C99 新增的内联函数(inline function)。这种内联函数相当于给编译器一个建议,告诉它将函数的代码插入到调用的地方。编译器可以选择忽略内联函数的建议,继续将函数编译为常规函数。

简单的说,对于常规函数编译器会将它解释成压栈,然后 call 函数名这样的指令;对于内联函数,编译器可以直接在调用处“展开”。这样就避免了压栈和 call 的开销。这样的设计让人很容易联想起来宏定义。

以一个例子来进行说明:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

inline void swapINT1( UINTN *p1, UINTN *p2 )       // 一个内联函数
{
   UINTN tmp = *p1; *p1 = *p2; *p2 = tmp;
}

void swapINT2( UINTN *p1, UINTN *p2 )       // 一个内联函数
{
   UINTN tmp = *p1; *p1 = *p2; *p2 = tmp;
}

INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
  UINTN a=12,b=34;
  swapINT1(&a,&b);
  swapINT2(&a,&b);

  return(0);
}

代码非常简单,定义了一个内联函数 swapINT1() 和一个普通函数swapINT2()。我们在编译种加入 /FAcs 让它生成汇编文件。

ShellAppMain PROC					; COMDAT

; 36   : {

$LN5:
  00000	48 89 54 24 10	 mov	 QWORD PTR [rsp+16], rdx
  00005	48 89 4c 24 08	 mov	 QWORD PTR [rsp+8], rcx
  0000a	48 83 ec 48	 sub	 rsp, 72			; 00000048H

; 37   :   UINTN a=12,b=34;

  0000e	48 c7 44 24 28
	0c 00 00 00	 mov	 QWORD PTR a$[rsp], 12
  00017	48 c7 44 24 20
	22 00 00 00	 mov	 QWORD PTR b$[rsp], 34	; 00000022H

; 14   :    UINTN tmp = *p1; *p1 = *p2; *p2 = tmp;

  00020	48 8b 44 24 28	 mov	 rax, QWORD PTR a$[rsp]
  00025	48 89 44 24 30	 mov	 QWORD PTR tmp$1[rsp], rax
  0002a	48 8b 44 24 20	 mov	 rax, QWORD PTR b$[rsp]
  0002f	48 89 44 24 28	 mov	 QWORD PTR a$[rsp], rax
  00034	48 8b 44 24 30	 mov	 rax, QWORD PTR tmp$1[rsp]
  00039	48 89 44 24 20	 mov	 QWORD PTR b$[rsp], rax

; 38   :   swapINT1(&amp;a,&amp;b);
; 39   :   swapINT2(&amp;a,&amp;b);

  0003e	48 8d 54 24 20	 lea	 rdx, QWORD PTR b$[rsp]
  00043	48 8d 4c 24 28	 lea	 rcx, QWORD PTR a$[rsp]
  00048	e8 00 00 00 00	 call	 swapINT2

; 40   : 
; 41   :   return(0);

  0004d	33 c0		 xor	 eax, eax

; 42   : }

  0004f	48 83 c4 48	 add	 rsp, 72			; 00000048H
  00053	c3		 ret	 0
ShellAppMain ENDP

从上面可以看到 swapINT1() 被“展开”了,swapINT2() 则是正常的通过 call 来调用的。

需要注意的是:内联函数对于编译器来说只是“建议”,编译器完全可以不接受这个建议。例如,r如果函数比较复杂,编译器不会遵从这个建议。

参考:

  1. https://www.cnblogs.com/zhuchunlin/p/17756920.html C语言 – 内联函数
  2. https://c.biancheng.net/view/339.html C语言内联函数

CH552 PWM 测试

根据资料,CH552 的PWM 频率是根据 PWM_CK_SE 的分频而来【参考1】。具体的频率计算方法是:

Fsys / 256 / PWM_CK_SE

对应的 DataSheet 描述如下:

为此,编写一段测试代码:

void setup() {
  // put your setup code here, to run once:
  pinMode(15,OUTPUT);
   PIN_FUNC &= ~(bPWM1_PIN_X);
   PWM_CTRL |= bPWM1_OUT_EN;

}

void loop() {
   PWM_CK_SE=1;
   PWM_DATA1 = 127;
   delay(10000);
   PWM_DATA1 = 63;
   delay(10000);
   PWM_CK_SE=16;
   PWM_DATA1 = 127;
   delay(10000);
   PWM_DATA1 = 63;
   delay(10000);
}

当前频率为 16,000,000/256/1=62.5KHz

频率和上面的相同,只是占空比不同。

当前频率为 16,000,000/256/16=3.906KHz

参考:

https://www.wch.cn/bbs/thread-67438-1.html