使用 Mini-F5333 制作usb游戏方向盘

分享作者:wx17371187533215
评测品牌:灵动微电子
评测型号:Mini-F5333
发布时间:2025-02-18 11:06:49
0
概要
本文将手把手教你如何将hid例程改造成游戏方向盘,将开发板上的三个电位器改造成方向盘转动,油门,刹车,并且游玩《欧洲卡车模拟2》
开源口碑分享内容

“喵呜?真的要让我给大家讲解嘛?”(猫猫疑惑地望向主人)“我可不一定能教会别人哦!”。

“尽管去给大家讲解吧,有难点我会补充的。”(将麦克风移到猫猫面前)

“嘿嘿!”(猫猫伸了个懒腰,趴在了电脑前,用尾巴勾住鼠标)“要开始喽~待会一定要奖励给我小鱼干喵!”

搭建环境

“在主人申请开发板的页面里有一个大大下载链接哦,只要我们点进去,就能找到全部的资料喵。”

“接下来,把库函数和例程下载下来,唔,还要原理图,也是很重要的东西,好像没有别打了吧。”(猫猫扶着脑袋开始思考,突然眼睛一亮)“还有keil包,虽然这个绿绿的软件很容易崩溃,界面也不好看,呆呆的,但主人真的很喜欢用它开发程序喵~这下就全搞定了嘿嘿”


“然后呢,把所有资料都解压一下,放在桌面吧,把这个keil pack双击安装一下,这样就能在keil里进行开发了”。(猫猫轻轻晃动尾巴点开MM32_KEIL_Pack_2.27文件夹)”好多文件喵,但是难不倒猫猫哦,只需要把要使用的MM32F5330的pack包安装一下就好喵。”

   注:pack疑似需要keil最低3.37版本,若低于3.37很可能无法正常安装。

“嗯哼哼~接下来就可以打开伟大灵动之神创建的神迹了。”(猫猫故弄玄虚的打开了LibSamples_MM32F5330_V0.13.7文件夹)

“第三个文件夹(Samples)是神之绘谱,它描绘出了外设的蓝图,御主User可以尽情使用他们,构建出属于自己独一无二的程序。”

“第二个文件(Device)是神之核,它无数诞于虚无的寄存器封装,构建,支撑起了整个灵动世界。”

“而第一个文件(3rdPartySoftwarePorting)则是神之连携,与其他的伟大存在交融,发展,共同将自身的意志向世人传播。”(一只手悄无声息地伸过猫猫头顶,拍了下去)

“喵呜~不闹了不闹了~接下来就要开始学习usb hid了。”

“在这个3rdPartySoftwarePorting中,包含了CANopenNode ---- Can总线协议,FreeRTOS----实时操作系统,TinyUSB----USB设备堆栈,他们各个都身怀绝技,而我们今天只会对TinyUSB进行一些更改,来完成我们的游戏方向盘,这样主人就不需要因为心心念念的游戏方向盘而削减猫猫的零食了,喵呜。”(无力吐槽的主人表示 †升天†)

“打开TinyUSB/Demo/TinyUSB_Device_HID_Comp,hid分类,符合了我们的要求,接下来就要改造一下它,”

“按照主人的教导,我们可以首先将这个例程完全的提取出来,重新建立一个空文件夹,把《CMSIS》、《MM32F5330》、《TinyUSB_Device_HID_Comp》、《tinyusb-0.16.0》复制一份,只需要这几个文件夹,我们就可以重塑usb-hid了。”

“打开了新的文件后,很多.c文件都找不到了喵,不过只需要让我们手动在更新一下.c.h文件的位置,就可以解决了。”


“好的!,只要这样,就不会因为找不到文件而报错了喵!诶,不对,这是什么,为什么有一个猫猫从没见过的错误!”(猫猫一脸迷茫的扭过头看像主人)。

注:例程中使用了keil分散加载文件的操作,固在图中所示的位置也需要额外再配置一个文件

“居然还有这种操作,真不愧是主人!好耶,编译通过了喵!,只要烧录进设备,串口就可以打印出设备信息了吧!我看看,是板子右侧的usb接口吧,它连接到了ch340芯片,是沁恒家的哦,主人超熟练他们usb的喵~,左侧的usb口直接与MM32芯片相连,接下来我们就用左侧的usb口进行usb通信了”。

“烧录器的话,就用主人新买的J-Link了。能值不少小鱼干呢,还是要略微配置一下的喵~一定要使用sw哦,不然会识别不到的。”

“嗯哼哼,下载完成了,只要接上右侧usb线就能看到串口数据,任意打开个串口助手就好,找到ch340的接口,我就不教各位去下载ch340驱动了哦”(猫猫回想到了在某度下载驱动被屠龙刀和坤坤追着跑的悲伤经历。)

“不对,不对,不对,为什么串口没有数据”。(猫猫用尾巴戳了RST键数次,串口纹丝不动。)

“喵呜~不能再求助主人了,让我仔细的检查下代码”(猫猫打开main.c文件,开始检查PLATFORM_Init()函数)。

“这个函数是一个初始化函数,里面分别对时钟,串口,led进行初始化并打印了设备信息,可能是uart初始化函数出现了问题吧,波特率115200,很顺口应该没错!”

“串口初始化,好像没有问题,PB6作为TX引脚嘛”(猫猫再次打开pdf图,视线停留在了CH340串口芯片上)。

“喵呜~ 串口芯片上明明连接的是PA9,这个例程怎么跟开发板对不上啊”(一脸黑线猫猫头.jpg)

注:我也很奇怪为什么所有例程都用的PB6呢,可能这个例程并不是专门为MINI-5333开发的。

要将一下PB6初始化代码给为PA9

{//原PB6
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);  // 使能GPIOA时钟
 GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_7);
 GPIO_StructInit(&GPIO_InitStruct);
    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_6;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_High;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &GPIO_InitStruct);
}
{//修改为PA9
 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);  // 使能GPIOA时钟
 GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_7);
 GPIO_StructInit(&GPIO_InitStruct);
    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_High;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
}

“这样一来就可以正常打印了喵~下一步就是配置一下adc采样,获取一下电位器的电平,对本喵来说简直小菜一碟,在例程文件夹Samples/LibSamples/ADC里有大量的采样例程喵~”

“好多adc例程啊,emmmm,我们要借用一下.........ADC_AnyChannel_OneCycleScan_DMA_Polling吧”(虽然猫猫的英文并不熟练,但凭借关键词还是能勉强看懂 这个例程可以配置多个通道,单次采集完这几个通道后,再通过DMA搬运数据。)

“只要略微修改,就完成了”

{
ADC_InitTypeDef  ADC_InitStruct;
    DMA_InitTypeDef  DMA_InitStruct;
    GPIO_InitTypeDef GPIO_InitStruct;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    ADC_CalibrationConfig(ADC1, 0x1FE); //这里是ADC校准函数,里面为什么是0x1FE,猫猫不清楚喵~

    ADC_StructInit(&ADC_InitStruct);
    ADC_InitStruct.ADC_Resolution = ADC_Resolution_12b;
    ADC_InitStruct.ADC_Prescaler  = ADC_Prescaler_16;
    ADC_InitStruct.ADC_Mode       = ADC_Mode_Scan;
    ADC_InitStruct.ADC_DataAlign  = ADC_DataAlign_Right;
    ADC_Init(ADC1, &ADC_InitStruct);

    ADC_DMACmd(ADC1, ENABLE);

//这里为每个通道配置采样时间,就是采样的3个通道了喵~
    ADC_SampleTimeConfig(ADC1, ADC_Channel_1, ADC_SampleTime_240_5);
    ADC_SampleTimeConfig(ADC1, ADC_Channel_4, ADC_SampleTime_240_5);
    ADC_SampleTimeConfig(ADC1, ADC_Channel_5, ADC_SampleTime_240_5);

//一共是3个通道呢,这里的2代表3个通道哦
    ADC_AnyChannelNumCfg(ADC1, 2);
    ADC_AnyChannelSelect(ADC1, ADC_AnyChannel_0, ADC_Channel_1);// 1,2,3乖乖站好
    ADC_AnyChannelSelect(ADC1, ADC_AnyChannel_1, ADC_Channel_4);
    ADC_AnyChannelSelect(ADC1, ADC_AnyChannel_2, ADC_Channel_5);
    ADC_AnyChannelCmd(ADC1, ENABLE);

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);

/* PA1(RV1) PA4(RV2) PA5(RV3) */
//把3个电位器的IO口配置为输入模式
    GPIO_StructInit(&GPIO_InitStruct);
    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_1 | GPIO_Pin_4 | GPIO_Pin_5;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_High;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    ADC_Cmd(ADC1, ENABLE);

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    DMA_DeInit(DMA1_Channel1);

    DMA_StructInit(&DMA_InitStruct);
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->ADDATA);
    DMA_InitStruct.DMA_MemoryBaseAddr     = (uint32_t)data;	//配置好DMA搬运地址
    DMA_InitStruct.DMA_DIR                = DMA_DIR_PeripheralSRC;
    DMA_InitStruct.DMA_BufferSize         = 3;//3个电位器,size是3就足够了
    DMA_InitStruct.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
    DMA_InitStruct.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStruct.DMA_MemoryDataSize     = DMA_MemoryDataSize_HalfWord;
    DMA_InitStruct.DMA_Mode               = DMA_Mode_Circular;
    DMA_InitStruct.DMA_Priority           = DMA_Priority_High;
    DMA_InitStruct.DMA_M2M                = DMA_M2M_Disable;
    DMA_InitStruct.DMA_Auto_Reload        = DMA_Auto_Reload_Disable;
    DMA_Init(DMA1_Channel1, &DMA_InitStruct);

    DMA_Cmd(DMA1_Channel1, ENABLE);
}

“精简了一下采集函数,这需要调用一次,就能采集一组数据了喵~”

void ADC_AnyChannel_OneCycleScan_DMA_Polling_Sample()
{
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);

	while (RESET == DMA_GetFlagStatus(DMA1_FLAG_TC1))
	{
	}

	DMA_ClearFlag(DMA1_FLAG_TC1);
}

“不过嘛,采集一次也许出现异常的数据喵~可以写一个简单的函数才减小误差。”

uint16_t ADC_Buffer[ADC_NUM]; //adc采集后的数据放在这里,还没有经过处理,不要误用喵~
uint16_t ADC_Real[ADC_NUM];//这里就存放已经处理过的数据,可以直接通过usb发送过喵~

void PotentiometerRead(void)
{
	static uint32_t total1, total2,total3;//存放数据和
	static uint16_t max1, max2, max3;//多次采样的最大值
	static uint16_t min1, min2, min3;//多次采样的最小值
	total1 = total2 = total3 = max1 = max2 = max3 = 0;
	min1 = min2 = min3 = 65535;
	for(uint8_t i = 0; i < ADC_TIMES; i++)	//多次采集,目前设定是10次
	{
		ADC_AnyChannel_OneCycleScan_DMA_Polling_Sample();
		if(min1 > ADC_Buffer[0])	min1 = ADC_Buffer[0];
		if(min2 > ADC_Buffer[1])	min2 = ADC_Buffer[1];
		if(min3 > ADC_Buffer[2])	min3 = ADC_Buffer[2];
		
		if(max1 < ADC_Buffer[0])    max1 = ADC_Buffer[0];
		if(max2 < ADC_Buffer[1])    max2 = ADC_Buffer[1];
		if(max3 < ADC_Buffer[1])    max3 = ADC_Buffer[2];
		
		total1 += ADC_Buffer[0];
		total2 += ADC_Buffer[1];
		total3 += ADC_Buffer[2];
	}
	
//去掉一个最大值~去掉一个最小值~平均数就是我们的Real数值喵!!!
	ADC_Real[0] = (total1 - min1 - max1)/(ADC_TIMES-2) / PEDAL_RATE;
	ADC_Real[1] = (total2 - min2 - max2)/(ADC_TIMES-2) / PEDAL_RATE;
	ADC_Real[2] = (total3 - min3 - max3)/(ADC_TIMES-2) / PEDAL_RATE;
//如果ADC_TIMES小于2,就爆炸吧,PEDAL_RATE是一个比例系数,用来将0~4095的采样数据放缩到0~255,这样一来,只需要一字节就足够了。

	printf("\r\nRV1 Voltage = %d  \tRV2 Voltage = %d  \tRV3 Voltage = %d", ADC_Real[0], ADC_Real[1], ADC_Real[2]);
}

“嘿嘿嘿~我们的电位器采样就完成了,要到重点了~USB配置哦”(按下猫头)

注:usb协议是一个十分宏伟壮观的协议,一本新华字典都放不下的复杂,猫猫也不怎么懂复杂的usb协议,但是只需要跟着猫猫的步骤好,甚至可以教你把离合器也设计出来。

“TinyUSB是一个需要实时性的usb喵~它会在usb中断中置位,然后在主循环处理任务。要知道对于usb这种设备来说,即使没有数据传输,也需要跟主机(电脑)进行空包通讯的,不然主机(电脑)就不认usb了哦。所以说千万不要卡死循环喵~”

注:在platform.c文件中,包含了一个PLATFORM_DelayMS的延时函数,而这个函数的特别之处在于他依靠了滴答中断实现延时,可以在mm32f53330_it.c中找到这样一个函数


//void SysTick_Handler(void)
//{
//    if (0 != PLATFORM_DelayTick)
//    {
//        PLATFORM_DelayTick--;
//    }
//}

震惊!这个滴答中断被注释掉了,于是如果你真的使用PLATFORM_DelayMS时,就会发现程序卡死了,因为 PLATFORM_DelayTick变量根本没有减少。

当全局搜索SysTick_Handler时,就发现它混在usb函数中,因为usb需要极高的实时性,所以它已经被夺舍了


“这个例程已经将usb配置的很完善了,我们只需要进入usb_descriptors.c,找到uint8_t const desc_hid_report[],这里存放的四个看起来像函数的东西其实是宏哦,不过我们不需要他们,全部注释掉就好喵~”

uint8_t const desc_hid_report[] =
{
//  TUD_HID_REPORT_DESC_KEYBOARD( HID_REPORT_ID(REPORT_ID_KEYBOARD         )),
//  TUD_HID_REPORT_DESC_MOUSE   ( HID_REPORT_ID(REPORT_ID_MOUSE            )),
//  TUD_HID_REPORT_DESC_CONSUMER( HID_REPORT_ID(REPORT_ID_CONSUMER_CONTROL )),
//  TUD_HID_REPORT_DESC_GAMEPAD ( HID_REPORT_ID(REPORT_ID_GAMEPAD          ))
//这个部分就是本喵新写的代码,只有像本喵这么聪明的生物才能看懂哦~
//一句一句解释很麻烦,那我就只把关键数据讲解一下吧
    0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
	0x09, 0x04,        // Usage (Joystick)
	0xA1, 0x01,        // Collection (Application)
	0x09, 0x02,        //   Usage (Mouse)
	0xA1, 0x00,        //   Collection (Physical)
	0x05, 0x01,        //     Usage Page (Generic Desktop Ctrls)
//0x09是什么意思呢,可以这么理解,他代表着usb发送给电脑的一个数值,这个数据可以设定最大值和最小值,而电脑根据最大值,最小值和接收到的值,可以计算出一个类似于进度条的效果,而这刚好与主人的方向盘的要求相符。
//这里连续写了3个0x09,分别代表 方向盘转动, 油门, 刹车。
		0x09, 0x31,        //     Usage (Y)
		0x09, 0x32,        //     Usage (Z)
		0x09, 0x33,        //     Usage (Rx)
//0x15表示最小值,我们的最小值是0, 0x25表示最大值,我们的最大值是0xFF
		0x15, 0x00,        //     Logical Minimum (0)
		0x25, 0xFF,        //     Logical Maximum (-1)
//上面的事逻辑最大/小值,下面的事物理最大/小值;讲解这两个概念比较复杂,我们把他们保持一致就好
		0x35, 0x00,        //     Physical Minimum (0)
		0x45, 0xFF,        //     Physical Maximum (-1)
//这里代表一个数据的大小,单位是位(bit),所以说这里是8位即一个字节
		0x75, 0x08,        //     Report Size (8)
//这里代表数据的个数,我们有3个
		0x95, 0x03,        //     Report Count (3)
		0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
	0xC0,              //   End Collection
	//结合上述所写,如果你需要增加变量,或减少,只需要继续添加(0x09),并修改
0x95后的数值就好。
	

//这边是按键,我们还为他创建了8个按键,但这里我们没有使用喵。
	0x05, 0x09,        //   Usage Page (Button)
//按键的序号是1~8,一共八个按键
	0x19, 0x01,        //   Usage Minimum (0x01)
	0x29, 0x08,        //   Usage Maximum (0x08)
//最小值是0,最大是1,按键只有按下和松开喵
	0x15, 0x00,        //   Logical Minimum (0)
	0x25, 0x01,        //   Logical Maximum (1)
// 1位就足够表达了
	0x75, 0x01,        //   Report Size (1)
//8个按键喵
	0x95, 0x08,        //   Report Count (8)
	0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
	0xC0,              // End Collection

//如果想继续扩充按键的话,要记得必须占满一个字节哦,如果你只需要四个按键,只需要4个位,但usb不允许你空着位喵,必须占满字节,不过也有办法将没用到的位定义为空下,但本喵不会!
};

“接下来就要准备usb的发送了,小鱼干就在眼前了喵!让我们着眼TinyUSB_Device_HID_Comp_Sample();这个usb函数”

void TinyUSB_Device_HID_Comp_Sample(void)
{
    printf("\r\nTest %s", __FUNCTION__);

    TinyUSB_Device_Configure();	//这里是TinyUSB的初始化函数。

    while (1)	//进入循环,不会出去的喵
    {
        tud_task(); // TinyUSB device task		usb的核心函数,不许被阻塞卡死,不然usb也跟着一起死。
        led_blinking_task();	//led闪烁,入不了猫猫的眼睛,无视
        hid_task();	//usb发送任务,就要这个函数里将数据发送出去的喵!重点来了!
    }
}

“查看一下 hid_task()的定义。”

void hid_task(void)
{
  // Poll every 10ms
  const uint32_t interval_ms = 10;//表示每十毫秒进入一次本函数,时间不够会return掉的哦
  static uint32_t start_ms = 0;
  Static uint8_t report_data[4];	//3个字节的adc值+一个字节的按钮值

  if ( board_millis() - start_ms < interval_ms) return; // not enough time
  start_ms += interval_ms;

  uint32_t const btn = board_button_read();	//这是一个按钮的读取,猫猫不管,猫猫心里只有电位器。

  // Remote wakeup
//  if ( tud_suspended() && btn )
//  {
//    // Wake up host if we are in suspend mode
//    // and REMOTE_WAKEUP feature is enabled by host
//    tud_remote_wakeup();
//  }else
//  {
    // Send the 1st of report chain, the rest will be sent by tud_hid_report_complete_cb()
//    send_hid_report(REPORT_ID_KEYBOARD, btn);
//  }
//这些用于按钮触发的函数就不管了,我们要一直发送电位器的值喵~
	
//    uint8_t itf = 0;


PotentiometerRead();//我们直接在这里获取adc采样的值,并填充进数组喵~
report_data[0] = ADC_Real[0];
report_data[1] = ADC_Real[1];
report_data[2] = ADC_Real[2];
//我们并没使用按钮,那就空着吧,也不碍事喵

//这就是usb发送函数。 将 report_id 设置为 0,因为我们没有用到哦,一定要定义了几个字节就发送几个字节哦,更多,或更少,都无法被电脑识别的喵~
    if (tud_hid_report(0, report_data, sizeof(report_data))) {
        // 报告成功放入发送队列
    } else {
        // 报告放入队列失败
    }
}
//这里一定要注意,这是紧跟着的函数,看起来是usb发送的回调函数之类的。一定要清空哦,不然会妨碍到数据发送的喵~
// Invoked when sent REPORT successfully to host
// Application can use this to send the next report
// Note: For composite reports, report[0] is report ID
void tud_hid_report_complete_cb(uint8_t instance, uint8_t const* report, uint16_t len)
{
  (void) instance;
  (void) len;

//  uint8_t next_report_id = report[0] + 1u;

//  if (next_report_id < REPORT_ID_COUNT)
//  {
//    send_hid_report(next_report_id, board_button_read());
//  }
}

“编译,烧录,在连接左侧usb线,搞定,电脑叮咚一声就识别成功了喵~猫猫真是个天才!”

“我们来测试一下吧!点击电脑桌面左下角的搜索键,搜索(joy),就能看到设置USB游戏控制器的字样。如果点开里有我们的方向盘,就成功了喵~”

じゃんじゃんじゃん!!!这里出现了TinyUSB Device,我们并没有去配置usb的名字,这是默认的名字喵~”

“点击属性就能打开面板!上面出现了3个可以移动的轴,猫猫转动一下电位器试试”(面对主人递来的螺丝刀,猫猫伸直了手指,秀出了指甲)。

“不需要哦~还是我的手指能好用,待会要是不给我小鱼干,晚上就要在你胸口再划两道~”(猫猫将指甲卡入电位器,轻轻转动,看着屏幕上移动的轴正在随之移动)


“完成喵~”(猫猫拆开了一包(劲仔香辣味小鱼干),缩进了主人的怀里)。

.........................

注:

在使用本例程中,我发现他的usb时钟初始化函数USB_DeviceClockInit()内包含对按键keyPA6和PA8的初始化,但显然我们的开发板并非这两个按键,这导致按照程序的写法,在hid_task()中无法读取我们开发的按键值,例程无法正常运行;但此时我发现了更恐怖的事情。我们在程序中检索RCC_AHBPeriph_GPIOA时钟,此时我们发现,从头到尾都没有使能GPIOA的时钟,那么在没有使能时钟情况下,依然配置PA6和PA8,并在hid_task()进行了对按钮的读取,在这种情况下,读取到了必然是错误的按键值,我严重怀疑,开发本例程的程序员开小差了!!!!


以上,就是猫猫对模拟usb hid方向盘的全部理解了。其实猫猫对方向盘根本不感兴趣,也根本不知道USB协议是什么,猫猫只是在爱着主人的一切喵~

全部评论
暂无评论
0/144