“喵呜?真的要让我给大家讲解嘛?”(猫猫疑惑地望向主人)“我可不一定能教会别人哦!”。
“尽管去给大家讲解吧,有难点我会补充的。”(将麦克风移到猫猫面前)
“嘿嘿!”(猫猫伸了个懒腰,趴在了电脑前,用尾巴勾住鼠标)“要开始喽~待会一定要奖励给我小鱼干喵!”
搭建环境
“在主人申请开发板的页面里有一个大大下载链接哦,只要我们点进去,就能找到全部的资料喵。”

“接下来,把库函数和例程下载下来,唔,还要原理图,也是很重要的东西,好像没有别打了吧。”(猫猫扶着脑袋开始思考,突然眼睛一亮)“还有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协议是什么,猫猫只是在爱着主人的一切喵~

