如何将你的 Uno 变成一个鼠标设备

之前介绍过如何使用外部加一些元件从而让你的 Uno编程一个USB设备【参考1】,美中不足的地方是:

1.模拟方式实现USB,占用非常多的资源,基本上无法做其他事情;
2.需要外部元件,做起来比较麻烦。

本文根据【参考1】提供的信息,介绍一下如何将Uno通过刷写Firmware的方法编程一个Mouse。

先说一下原理:在标准的Uno上有一个USB转串口的芯片,Atmega 16U2 。通过对其重写Firmware,能够实现其他 USB 功能。重写之后这个芯片对AtMega 328 主芯片的通讯仍然是串口,就是说我们主芯片可以像串口通讯一样,直接发送数据到这个芯片上,这个芯片负责将数据转化为 USB信号发给PC。

在整个开始之前,首先,你必须有一个烧写器。这里推荐UsbTinyIsp,我使用的是 OCROBOT 出品的【参考2】。必须有这个东西,否则你一定会出现搞坏了没办法救回来的情况。

然后就可以使用工具刷新 16U2 的 Firmware。 这里推荐 AvrDude 这个工具,比较好的地方在于,这个工具有自动识别芯片的功能,你可以用它检查接线之类是否正常。选中Arduino-mouse.hex,然后点击Write即可,下方会有信息提示(这种工具本质都是调用AVRDUDE的命令行参数,只是AVRDUDE命令行非常复杂也不直观)。完成之后会出现下面的提示:
avrdude.exe: verifying …
avrdude.exe: 3872 bytes of flash verified
avrdude.exe done. Thank you.

接下来,在Arduino的IDE中打开Mouse_usb_demo.pde。
你需要确定 :
1.Tools->Board->选择的是Arduino Uno
2.Tools->Programmer下面选择的是 UsbTinyISP。
正常编译这个程序,不要选择编译并且上传,单纯选择编译即可。编译后,用File->Upload using Programmer 即完成上传。

再把Uno插在PC上,你会发现设备管理器中多出一个鼠标

uno1

PID VID如下所示:

uno2

实现的功能是你的鼠标每隔一段会自己转一圈。

完整的代码下载
Arduino-mouse

从这篇介绍也可以看出来,初学者尽量买原版的 Arduino Uno ,不要购买什么极客板等等,虽然会便宜一些,但是扩展性可玩性上下降很多。

参考:

1. http://hunt.net.nz/users/darran/weblog/cca39/Arduino_UNO_Mouse_HID.html Arduino UNO Mouse HID

2.这个东西是必须的,推荐这款有数字签名的驱动,支持Win8 64位,这样你就不必每次跑去关闭数字签名了。购买地址:
https://item.taobao.com/item.htm?spm=a1z10.5-c.w4002-684314961.37.VJHdrg&id=19035872312 。
当然淘宝上还有更便宜,我之前买了一个便宜的,说是无需驱动,结果无法用,非常懊恼……

用示波器“看” arduino (3) — PWM

Arduino 上面有模拟到数字(ADC)的采样功能但是没有数字到模拟的输出(DAC),在要求不是特别高的情况下 PWM 充当这一角色。

首先需要知道的是:不是所有的Pin都可以用来输出PWM,根据【参考1】,在Uno上的 3 5 6 9 10 和11才可以用来输出PWM.

image001

编写一个简单的程序来调整占空比

int  n=255;
const int PWMPin=6;

void setup()
{
    Serial.begin(9600);
    pinMode(PWMPin,OUTPUT);      //该端口需要选择有#号标识的数字口
}

void loop()
{
  char  c;

    while (Serial.available() > 0)  
    {
        c=Serial.read();
        if (']'==c) 
          {
            n=n+5;
          }
        if ('['==c) 
          {
            n=n-5;
          }
       if (n>255) {n=0;}
       if (n<0) {n=255;}   
       analogWrite(PWMPin,n); 
       Serial.println(n);

    }
}

我们使用 DFRobot 出品的 RoMeoBLE V1.0 作为实验器材。

占空比 250 时 , 250/255=98.039% ,测量值符合预期

image006

占空比 200 时 ,200/255=78.431% ,测量值符合预期

image003

占空比为0 时是一条低电平的直线,占空比255是一条高电平的直线,这里就不贴上来了。

不过特别注意到测量出来的频率是976.5Hz, 我更换了一个普通的UNO 结果还是 976HZ。这和很多资料中提到的 490Hz不同,经过研究,在【参考2】上找到了介绍,原来 D5 D6 的默认频率和其他的不同。换成 Pin6 修改上面的程序,看到的就是 490Hz了。

image004

同样,根据【参考2】改一下频率,可以看到当前是 31.36884 KHz

int  n=255;
const int PWMPin=9;

void setup()
{
    Serial.begin(9600);
    TCCR1B = TCCR1B & B11111000 | B00000001;    // set timer 1 divisor to 1 for PWM frequency of 31372.55 Hz    
    pinMode(PWMPin,OUTPUT);      //该端口需要选择有#号标识的数字口
}

void loop()
{
  char  c;

    while (Serial.available() > 0)  
    {
        c=Serial.read();
        if (']'==c) 
          {
            n=n+5;
          }
        if ('['==c) 
          {
            n=n-5;
          }
       if (n>255) {n=0;}
       if (n<0) {n=255;}   
       analogWrite(PWMPin,n); 
       Serial.println(n);

    }
}

调整占空比为 245 时的样子

image005

放大一点看看具体波形

image006

参考:
1. http://www.diyleyuan.com/index.php?m=content&c=index&a=show&catid=29&id=616
2. https://arduino-info.wikispaces.com/Arduino-PWM-Frequency
pwma
3. http://playground.arduino.cc/Code/PwmFrequency 关于调整频率官方的介绍
4. https://www.arduino.cc/en/Tutorial/SecretsOfArduinoPWM Secrets of Arduino PWM
http://www.diy-robots.com/?p=852 上面文章的翻译
http://www.diy-robots.com/?p=814 上面文章的翻译

Step to UEFI (69) —– 动态加载修改Application

标题看起来非常拗口,具体来说描述起来就是下面的问题:

“我想写一个简单的程序,先把某个app的Load进内存,然后在内存里爆搜一个特征字串,搜到之后将该内存第一个字节替换。以下为代码片段,碰到一个问题就是,我搜到特征字串之后,修改其内存的内容一直改不了,请问各位大大,是不是UEFI有相应的保护策略,不能修改LoadImage的内存?我个人觉得是不应该,因为我是LoadImage的宿主,我Load的内存应该是可以被我修改的。请大牛们指教啊!!!

Status=gBS->LoadImage(TRUE, ImageHandle, DstDevicePath, NULL, 0, &DstImageHandle); //LoadImage
if (!EFI_ERROR(Status))
{
Print(L”Load Image success\n”);
}
Status=gBS->HandleProtocol(DstImageHandle, &gEfiLoadedImageProtocolGuid,(void **) &LoadedImage);
if (EFI_ERROR(Status)) {
Print(L”Can not retrieve a LoadedImageProtocol handle for ImageHandle\n”);
gBS->Exit(ImageHandle,EFI_SUCCESS,0,NULL);
}
//Get the loaded image base address
imageBase=LoadedImage->ImageBase;
size=LoadedImage->ImageSize;

temp=(char *)imageBase;

//Search the sig;, replace the first byte
for (; temp<(char *)imageBase+size; temp++) { if (*temp==0x55 && *(temp+1)==0x00 && *(temp+2)==0x45 && *(temp+3)==0x00) { Print(L"Find sig\n"); *temp=0x45; Print(L"addr %x\n",temp); break; } } 上述问题来自【参考1】 这个一个有趣的问题,也不知道那个朋友最后是否成功。根据上面的问题,我做一下实验。 首先,准备一个被修改的 App。当然,根据之前的知识,这个 App 不能使用 CLIB 库,目前为止我还是不知道为什么无法加载调用这个库的 Application.

#include <Uefi.h>
#include <Library/PcdLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>


/**
  The user Entry Point for Application. The user code starts with this function
  as the real entry point for the application.

  @param[in] ImageHandle    The firmware allocated handle for the EFI image.  
  @param[in] SystemTable    A pointer to the EFI System Table.
  
  @retval EFI_SUCCESS       The entry point is executed successfully.
  @retval other             Some error occurs when executing this entry point.

**/
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
                                       
  CHAR8  *s1= "This code comes from www.lab-z.com";
  CHAR16 *s2=L"                                  ";
  CHAR16 *Result;
  
  Result=AsciiStrToUnicodeStr(s1,s2);
  Print(L"%s\n",s2);

  return EFI_SUCCESS;
}

 

代码非常简单,将一个 ASCII 字符串转化为 Unicode 的,然后显示出来。使用 ASCII 的原因是为了便于查找。

上面的程序编译之后,使用十六进制工具打开可以直接查看到 ASCII 字符。

exec3

然后,继续编写加载和修改的程序如下:

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

#include  <stdio.h>
#include  <stdlib.h>
#include  <wchar.h>

#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>

extern EFI_BOOT_SERVICES           	 *gBS;
extern EFI_SYSTEM_TABLE				 *gST;
extern EFI_RUNTIME_SERVICES 		 *gRT;

extern EFI_SHELL_PROTOCOL            *gEfiShellProtocol;
extern EFI_SHELL_ENVIRONMENT2 		 *mEfiShellEnvironment2;

extern EFI_HANDLE					 gImageHandle;
/**
  GET  DEVICEPATH
**/
EFI_DEVICE_PATH_PROTOCOL *
EFIAPI
ShellGetDevicePath (
  IN CHAR16                     * CONST DeviceName OPTIONAL
  )
{
  //
  // Check for UEFI Shell 2.0 protocols
  //
  if (gEfiShellProtocol != NULL) {
    return (gEfiShellProtocol->GetDevicePathFromFilePath(DeviceName));
  }

  //
  // Check for EFI shell
  //
  if (mEfiShellEnvironment2 != NULL) {
    return (mEfiShellEnvironment2->NameToPath(DeviceName));
  }

  return (NULL);
}

int
EFIAPI
main (
  IN int Argc,
  IN char **Argv
  )
{
  EFI_DEVICE_PATH_PROTOCOL 	*DevicePath;
  EFI_HANDLE				NewHandle;
  EFI_STATUS				Status;
  UINTN			ExitDataSizePtr;  
  CHAR16 					*R=L"HelloWorld.efi";
  EFI_LOADED_IMAGE_PROTOCOL	*ImageInfo = NULL;
  CHAR8						*temp;
  
  Print(L"File [%s]\n",R);

  DevicePath=ShellGetDevicePath(R);

  //
  // Load the image with:
  // FALSE - not from boot manager and NULL, 0 being not already in memory
  //
  Status = gBS->LoadImage(
    FALSE,
    gImageHandle,
    DevicePath,
    NULL,
    0,
    &NewHandle);  

  if (EFI_ERROR(Status)) {
    if (NewHandle != NULL) {
      gBS->UnloadImage(NewHandle);
    }
	Print(L"Error during LoadImage [%X]\n",Status);
    return (Status);
  }

  Status = gBS -> HandleProtocol (
						NewHandle,
						&gEfiLoadedImageProtocolGuid,
						&ImageInfo
						);
  Print(L"ImageBase [%lX]\n",ImageInfo->ImageBase);
  Print(L"ImageSize [%lX]\n",ImageInfo->ImageSize);

  temp=(char *)ImageInfo->ImageBase;
  //Search the sig;, replace the first byte
  for (; temp<(char *)ImageInfo->ImageBase+ImageInfo->ImageSize; temp++)
        {
				//"lab" 6C 61 62
                if (*temp==0x6C && *(temp+1)==0x61 && *(temp+2)==0x62)
                {
                        Print(L"Find sig\n");
                        Print(L"addr %x\n",temp);
						*(temp  )=0x2D;
						*(temp+1)=0x2D;
						*(temp+2)=0x2D;
                        break;
                } 
        }  
  
  
  //
  // now start the image, passing up exit data if the caller requested it
  //
  Status = gBS->StartImage(
                     NewHandle,
                     &ExitDataSizePtr,
                     NULL
              );
  if (EFI_ERROR(Status)) {
    if (NewHandle != NULL) {
      gBS->UnloadImage(NewHandle);
    }
	Print(L"Error during StartImage [%X]\n",Status);
    return (Status);
  }
  
  gBS->UnloadImage (NewHandle);  
  return EFI_SUCCESS;
}

 

加载部分的代码使用的是 【参考2】的框架,搜索部分的代码用的是前面问题中给出来的示例。我们在加载后的 Application 的空间中搜索 “lab” 字符串并且替换为 “—”。

运行结果如下,我们先执行了一次 HelloWorld.efi,可以看到他能正常打印字符串,之后再用我们的程序加载一次,可以看到字符串被修改掉了。

exec3r

看起来并没有什么保护之类的,轻而易举的改掉了 Application 的内容。猜测之前提出问题的朋友有可能是被加载的代码用到了 CLIB, 或者是代码中的字符串是按照 UNICODE 给出来的,所以无法找到。

这样的动态加载可以用在一些特殊的地方,比如,我见过一款 DOS 下的测试软件,有一个主程序 EXE 和 n多个独立的 EXE 构成。主程序可以调用其他的 EXE 进行测试,但是单独的 EXE 无法执行,这样的好处是开发时可以独立开发单独模块,分发之后有主程序进行控制只在需要的环境中运行。

本文提到的代码下载

exec3

HelloWorld

参考:

1. http://biosren.com/thread-4564-1-31.html
2. http://www.lab-z.com/efiloadedimageprotocol/ Step to UEFI (46) —– EFILOADEDIMAGEPROTOCOL的使用