- enum
- {
- IR_CODE_STEP_IDLE = 0,
- IR_CODE_STEP_START,
- IR_CODE_STEP_DATA,
- IR_CODE_STEP_STOP,
- };
- enum
- {
- IR_CODE_FLAG_NONE = 0,
- IR_CODE_FLAG_CMD,
- IR_CODE_FLAG_REPEAT,
- };
- #define MAX_TIME_DIFF 60
- uint32_t IR_time_us = 0;
- uint32_t IR_H_time_us = 0;
- uint32_t IR_L_time_us = 0;
- uint8_t IR_code[4];
- uint8_t IR_code_index = 0;
- uint8_t IR_code_bit = 0;
- uint8_t IR_code_step = IR_CODE_STEP_IDLE;
- uint8_t IR_lev_last = 0;
- uint8_t IR_code_flag = IR_CODE_FLAG_NONE;
- uint8_t IRIsTimeInRange(uint32_t time,uint32_t targettime,uint32_t diff)
- {
- if(time < targettime && targettime - time > diff)
- return 0;
- if(time > targettime && time - targettime > diff)
- return 0;
- return 1;
- }
- void IRTimeCount(uint32_t us)
- {
- IR_time_us += us;
- }
- void IRDataProc(void)
- {
- switch(IR_code_step)
- {
- case IR_CODE_STEP_IDLE:
- if(IRIsTimeInRange(IR_H_time_us,9000,MAX_TIME_DIFF) != 0)
- {
- IR_code_step = IR_CODE_STEP_START;
- IR_code_flag = IR_CODE_FLAG_NONE;
- }
- break;
- case IR_CODE_STEP_START:
- if(IRIsTimeInRange(IR_L_time_us,4500,MAX_TIME_DIFF) != 0)
- {
- //开始指令
- memset(IR_code,0,4);
- IR_code_index = 0;
- IR_code_bit = 0;
- IR_code_step = IR_CODE_STEP_DATA;
- }
- else if(IRIsTimeInRange(IR_L_time_us,2250,MAX_TIME_DIFF) != 0)
- {
- //重复指令
- IR_code_step = IR_CODE_STEP_STOP;
- IR_code_flag = IR_CODE_FLAG_REPEAT;
- }
- else
- {
- IR_code_step = IR_CODE_STEP_IDLE;
- IR_code_flag = IR_CODE_FLAG_NONE;
- }
- break;
- case IR_CODE_STEP_DATA:
- if(IRIsTimeInRange(IR_H_time_us,560,MAX_TIME_DIFF) != 0)
- {
- if(IRIsTimeInRange(IR_L_time_us,1690,MAX_TIME_DIFF) != 0)
- {
- IR_code[IR_code_index] |= (1<<IR_code_bit);
- }
- else if(IRIsTimeInRange(IR_L_time_us,560,MAX_TIME_DIFF) == 0)
- {
- IR_code_step = IR_CODE_STEP_IDLE;
- IR_code_flag = IR_CODE_FLAG_NONE;
- }
- IR_code_bit += 1;
- if(IR_code_bit == 8)
- {
- IR_code_bit = 0;
- IR_code_index += 1;
- if(IR_code_index == 4)
- {
- IR_code_step = IR_CODE_STEP_STOP;
- IR_code_flag = IR_CODE_FLAG_CMD;
- }
- }
- }
- else
- {
- IR_code_step = IR_CODE_STEP_IDLE;
- IR_code_flag = IR_CODE_FLAG_NONE;
- }
- break;
- case IR_CODE_STEP_STOP:
- if(IRIsTimeInRange(IR_H_time_us,560,MAX_TIME_DIFF) != 0)
- {
- //结束指令
- IR_code_step = IR_CODE_STEP_IDLE;
- }
- else
- {
- IR_code_step = IR_CODE_STEP_IDLE;
- IR_code_flag = IR_CODE_FLAG_NONE;
- }
- break;
- default:
- IR_code_step = IR_CODE_STEP_IDLE;
- break;
- }
- }
- void IRIOcheck(uint8_t lev)
- {
- if(lev != IR_lev_last)
- {
- IR_lev_last = lev;
- if(lev == 0)
- {
- IR_H_time_us = IR_time_us;
- if(IR_code_step == IR_CODE_STEP_IDLE || IR_code_step == IR_CODE_STEP_STOP)
- {
- IRDataProc();
- }
- }
- else
- {
- if(IR_code_step == IR_CODE_STEP_IDLE)
- {
- IR_L_time_us = 0;
- }
- else
- {
- IR_L_time_us = IR_time_us;
- IRDataProc();
- }
- }
- IR_time_us = 0;
- }
- }
通过STM32CubeMX配置一个定时器用来对高低电平进行计时,为了避免太过频繁的进入中断,这里配置的周期是20us
在定时器中断函数里调用IRTimeCount传入参数为定时器触发的周期
- void TIM4_IRQHandler(void)
- {
- /* USER CODE BEGIN TIM4_IRQn 0 */
- if(LL_TIM_IsActiveFlag_UPDATE(TIM4) == SET)
- {
- LL_TIM_ClearFlag_UPDATE(TIM4);
- IRTimeCount(20);
- }
- /* USER CODE END TIM4_IRQn 0 */
- /* USER CODE BEGIN TIM4_IRQn 1 */
- /* USER CODE END TIM4_IRQn 1 */
- }
配置检测红外信号的引脚,我这里使用的是PB9开启了双边沿中断
触发中断后检测PB9的电平,需要注意的是要做取反处理
- uint8_t ir_pin_it_flag = 0;
- void EXTI9_5_IRQHandler(void)
- {
- /* USER CODE BEGIN EXTI9_5_IRQn 0 */
- /* USER CODE END EXTI9_5_IRQn 0 */
- if (LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_9) != RESET)
- {
- LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_9);
- /* USER CODE BEGIN LL_EXTI_LINE_9 */
- ir_pin_it_flag = 1;
- /* USER CODE END LL_EXTI_LINE_9 */
- }
- /* USER CODE BEGIN EXTI9_5_IRQn 1 */
- /* USER CODE END EXTI9_5_IRQn 1 */
- }
- void IRAPP(void)
- {
- if(ir_pin_it_flag != 0)
- {
- ir_pin_it_flag = 0;
- IRIOcheck(!LL_GPIO_IsInputPinSet(IR_PIN_GPIO_Port,IR_PIN_Pin));
- }
- }
如果不用中断就直接轮询PB9的状态
- void IRAPP(void)
- {
- IRIOcheck(!LL_GPIO_IsInputPinSet(IR_PIN_GPIO_Port,IR_PIN_Pin));
- }
在主循环中调用IRAPP(),检测到正确的红外控制命令后IR_code_flag会被置位,先通过串口打印看看效果
- if(IR_code_flag == IR_CODE_FLAG_CMD)
- {
- printf("rev ir data: %02X%02X%02X%02X\r\n",IR_code[0],IR_code[1],IR_code[2],IR_code[3]);
- IR_code_flag = IR_CODE_FLAG_NONE;
- }
- else if(IR_code_flag == IR_CODE_FLAG_REPEAT)
- {
- printf("rev ir repeat\r\n");
- }
接下来实现用STM32F103模拟键盘,在STM32CubeMX中开启USBDevice
选择HID
生成的代码默认设备类型是鼠标,找个键盘的描述符替换掉usbd_hid.c中的鼠标描述符
- //#define HID_MOUSE_REPORT_DESC_SIZE 74U
- #define HID_MOUSE_REPORT_DESC_SIZE 63U
- /*修改usbd_hid.c中的报告设备描述符*/
- __ALIGN_BEGIN static uint8_t HID_MOUSE_ReportDesc[HID_MOUSE_REPORT_DESC_SIZE] __ALIGN_END =
- {
- 0x05, 0x01, // USAGE_PAGE (Generic Desktop) //63
- 0x09, 0x06, // USAGE (Keyboard)
- 0xa1, 0x01, // COLLECTION (Application)
- 0x05, 0x07, // USAGE_PAGE (Keyboard)
- 0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
- 0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
- 0x15, 0x00, // LOGICAL_MINIMUM (0)
- 0x25, 0x01, // LOGICAL_MAXIMUM (1)
- 0x75, 0x01, // REPORT_SIZE (1)
- 0x95, 0x08, // REPORT_COUNT (8)
- 0x81, 0x02, // INPUT (Data,Var,Abs)
- 0x95, 0x01, // REPORT_COUNT (1)
- 0x75, 0x08, // REPORT_SIZE (8)
- 0x81, 0x03, // INPUT (Cnst,Var,Abs)
- 0x95, 0x05, // REPORT_COUNT (5)
- 0x75, 0x01, // REPORT_SIZE (1)
- 0x05, 0x08, // USAGE_PAGE (LEDs)
- 0x19, 0x01, // USAGE_MINIMUM (Num Lock)
- 0x29, 0x05, // USAGE_MAXIMUM (Kana)
- 0x91, 0x02, // OUTPUT (Data,Var,Abs)
- 0x95, 0x01, // REPORT_COUNT (1)
- 0x75, 0x03, // REPORT_SIZE (3)
- 0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
- 0x95, 0x06, // REPORT_COUNT (6)
- 0x75, 0x08, // REPORT_SIZE (8)
- 0x15, 0x00, // LOGICAL_MINIMUM (0)
- 0x25, 0x65, // LOGICAL_MAXIMUM (101)
- 0x05, 0x07, // USAGE_PAGE (Keyboard)
- 0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
- 0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
- 0x81, 0x00, // INPUT (Data,Ary,Abs)
- 0xc0, // END_COLLECTION
- };
文件中USBD_HID_OtherSpeedCfgDesc这两处也改掉
看一下键盘HID消息格式
找一份键盘值映射代码
- typedef enum
- {
- KEYBOARD_NONE = 0,
- KEYBOARD_ERROR_ROLL_OVER,
- KEYBOARD_POST_FAIL,
- KEYBOARD_ERROR_UNDEFINED,
- KEYBOARD_A,
- KEYBOARD_B,
- KEYBOARD_C,
- KEYBOARD_D,
- KEYBOARD_E,
- KEYBOARD_F,
- KEYBOARD_G,
- KEYBOARD_H,
- KEYBOARD_I,
- KEYBOARD_J,
- KEYBOARD_K,
- KEYBOARD_L,
- KEYBOARD_M,
- KEYBOARD_N,
- KEYBOARD_O,
- KEYBOARD_P,
- KEYBOARD_Q,
- KEYBOARD_R,
- KEYBOARD_S,
- KEYBOARD_T,
- KEYBOARD_U,
- KEYBOARD_V,
- KEYBOARD_W,
- KEYBOARD_X,
- KEYBOARD_Y,
- KEYBOARD_Z,
- KEYBOARD_1_EXCLAMATION,
- KEYBOARD_2_AT,
- KEYBOARD_3_NUMBER_SIGN,
- KEYBOARD_4_DOLLAR,
- KEYBOARD_5_PERCENT,
- KEYBOARD_6_CARET,
- KEYBOARD_7_AMPERSAND,
- KEYBOARD_8_ASTERISK,
- KEYBOARD_9_OPARENTHESIS,
- KEYBOARD_0_CPARENTHESIS,
- KEYBOARD_ENTER,
- KEYBOARD_ESCAPE,
- KEYBOARD_BACKSPACE,
- KEYBOARD_TAB,
- KEYBOARD_SPACEBAR,
- KEYBOARD_MINUS_UNDERSCORE,
- KEYBOARD_EQUAL_PLUS,
- KEYBOARD_OBRACKET_AND_OBRACE,
- KEYBOARD_CBRACKET_AND_CBRACE,
- KEYBOARD_BACKSLASH_VERTICAL_BAR,
- KEYBOARD_NONUS_NUMBER_SIGN_TILDE,
- KEYBOARD_SEMICOLON_COLON,
- KEYBOARD_SINGLE_AND_DOUBLE_QUOTE,
- KEYBOARD_GRAVE_ACCENT_AND_TILDE,
- KEYBOARD_COMMA_AND_LESS,
- KEYBOARD_DOT_GREATER,
- KEYBOARD_SLASH_QUESTION,
- KEYBOARD_CAPS_LOCK,
- KEYBOARD_F1,
- KEYBOARD_F2,
- KEYBOARD_F3,
- KEYBOARD_F4,
- KEYBOARD_F5,
- KEYBOARD_F6,
- KEYBOARD_F7,
- KEYBOARD_F8,
- KEYBOARD_F9,
- KEYBOARD_F10,
- KEYBOARD_F11,
- KEYBOARD_F12,
- KEYBOARD_PRINTSCREEN,
- KEYBOARD_SCROLL_LOCK,
- KEYBOARD_PAUSE,
- KEYBOARD_INSERT,
- KEYBOARD_HOME,
- KEYBOARD_PAGEUP,
- KEYBOARD_DELETE,
- KEYBOARD_END1,
- KEYBOARD_PAGEDOWN,
- KEYBOARD_RIGHTARROW,
- KEYBOARD_LEFTARROW,
- KEYBOARD_DOWNARROW,
- KEYBOARD_UPARROW,
- KEYBOARD_KEYBOARDPAD_NUM_LOCK_AND_CLEAR,
- KEYBOARD_KEYBOARDPAD_SLASH,
- KEYBOARD_KEYBOARDPAD_ASTERIKS,
- KEYBOARD_KEYBOARDPAD_MINUS,
- KEYBOARD_KEYBOARDPAD_PLUS,
- KEYBOARD_KEYBOARDPAD_ENTER,
- KEYBOARD_KEYBOARDPAD_1_END,
- KEYBOARD_KEYBOARDPAD_2_DOWN_ARROW,
- KEYBOARD_KEYBOARDPAD_3_PAGEDN,
- KEYBOARD_KEYBOARDPAD_4_LEFT_ARROW,
- KEYBOARD_KEYBOARDPAD_5,
- KEYBOARD_KEYBOARDPAD_6_RIGHT_ARROW,
- KEYBOARD_KEYBOARDPAD_7_HOME,
- KEYBOARD_KEYBOARDPAD_8_UP_ARROW,
- KEYBOARD_KEYBOARDPAD_9_PAGEUP,
- KEYBOARD_KEYBOARDPAD_0_INSERT,
- KEYBOARD_KEYBOARDPAD_DECIMAL_SEPARATOR_DELETE,
- KEYBOARD_NONUS_BACK_SLASH_VERTICAL_BAR,
- KEYBOARD_APPLICATION,
- KEYBOARD_POWER,
- KEYBOARD_KEYBOARDPAD_EQUAL,
- KEYBOARD_F13,
- KEYBOARD_F14,
- KEYBOARD_F15,
- KEYBOARD_F16,
- KEYBOARD_F17,
- KEYBOARD_F18,
- KEYBOARD_F19,
- KEYBOARD_F20,
- KEYBOARD_F21,
- KEYBOARD_F22,
- KEYBOARD_F23,
- KEYBOARD_F24,
- KEYBOARD_EXECUTE,
- KEYBOARD_HELP,
- KEYBOARD_MENU,
- KEYBOARD_SELECT,
- KEYBOARD_STOP,
- KEYBOARD_AGAIN,
- KEYBOARD_UNDO,
- KEYBOARD_CUT,
- KEYBOARD_COPY,
- KEYBOARD_PASTE,
- KEYBOARD_FIND,
- KEYBOARD_MUTE,
- KEYBOARD_VOLUME_UP,
- KEYBOARD_VOLUME_DOWN,
- KEYBOARD_LOCKING_CAPS_LOCK,
- KEYBOARD_LOCKING_NUM_LOCK,
- KEYBOARD_LOCKING_SCROLL_LOCK,
- KEYBOARD_KEYBOARDPAD_COMMA,
- KEYBOARD_KEYBOARDPAD_EQUAL_SIGN,
- KEYBOARD_INTERNATIONAL1,
- KEYBOARD_INTERNATIONAL2,
- KEYBOARD_INTERNATIONAL3,
- KEYBOARD_INTERNATIONAL4,
- KEYBOARD_INTERNATIONAL5,
- KEYBOARD_INTERNATIONAL6,
- KEYBOARD_INTERNATIONAL7,
- KEYBOARD_INTERNATIONAL8,
- KEYBOARD_INTERNATIONAL9,
- KEYBOARD_LANG1,
- KEYBOARD_LANG2,
- KEYBOARD_LANG3,
- KEYBOARD_LANG4,
- KEYBOARD_LANG5,
- KEYBOARD_LANG6,
- KEYBOARD_LANG7,
- KEYBOARD_LANG8,
- KEYBOARD_LANG9,
- KEYBOARD_ALTERNATE_ERASE,
- KEYBOARD_SYSREQ,
- KEYBOARD_CANCEL,
- KEYBOARD_CLEAR,
- KEYBOARD_PRIOR,
- KEYBOARD_RETURN,
- KEYBOARD_SEPARATOR,
- KEYBOARD_OUT,
- KEYBOARD_OPER,
- KEYBOARD_CLEAR_AGAIN,
- KEYBOARD_CRSEL,
- KEYBOARD_EXSEL,
- KEYBOARD_RESERVED1,
- KEYBOARD_RESERVED2,
- KEYBOARD_RESERVED3,
- KEYBOARD_RESERVED4,
- KEYBOARD_RESERVED5,
- KEYBOARD_RESERVED6,
- KEYBOARD_RESERVED7,
- KEYBOARD_RESERVED8,
- KEYBOARD_RESERVED9,
- KEYBOARD_RESERVED10,
- KEYBOARD_RESERVED11,
- KEYBOARD_KEYBOARDPAD_00,
- KEYBOARD_KEYBOARDPAD_000,
- KEYBOARD_THOUSANDS_SEPARATOR,
- KEYBOARD_DECIMAL_SEPARATOR,
- KEYBOARD_CURRENCY_UNIT,
- KEYBOARD_CURRENCY_SUB_UNIT,
- KEYBOARD_KEYBOARDPAD_OPARENTHESIS,
- KEYBOARD_KEYBOARDPAD_CPARENTHESIS,
- KEYBOARD_KEYBOARDPAD_OBRACE,
- KEYBOARD_KEYBOARDPAD_CBRACE,
- KEYBOARD_KEYBOARDPAD_TAB,
- KEYBOARD_KEYBOARDPAD_BACKSPACE,
- KEYBOARD_KEYBOARDPAD_A,
- KEYBOARD_KEYBOARDPAD_B,
- KEYBOARD_KEYBOARDPAD_C,
- KEYBOARD_KEYBOARDPAD_D,
- KEYBOARD_KEYBOARDPAD_E,
- KEYBOARD_KEYBOARDPAD_F,
- KEYBOARD_KEYBOARDPAD_XOR,
- KEYBOARD_KEYBOARDPAD_CARET,
- KEYBOARD_KEYBOARDPAD_PERCENT,
- KEYBOARD_KEYBOARDPAD_LESS,
- KEYBOARD_KEYBOARDPAD_GREATER,
- KEYBOARD_KEYBOARDPAD_AMPERSAND,
- KEYBOARD_KEYBOARDPAD_LOGICAL_AND,
- KEYBOARD_KEYBOARDPAD_VERTICAL_BAR,
- KEYBOARD_KEYBOARDPAD_LOGIACL_OR,
- KEYBOARD_KEYBOARDPAD_COLON,
- KEYBOARD_KEYBOARDPAD_NUMBER_SIGN,
- KEYBOARD_KEYBOARDPAD_SPACE,
- KEYBOARD_KEYBOARDPAD_AT,
- KEYBOARD_KEYBOARDPAD_EXCLAMATION_MARK,
- KEYBOARD_KEYBOARDPAD_MEMORY_STORE,
- KEYBOARD_KEYBOARDPAD_MEMORY_RECALL,
- KEYBOARD_KEYBOARDPAD_MEMORY_CLEAR,
- KEYBOARD_KEYBOARDPAD_MEMORY_ADD,
- KEYBOARD_KEYBOARDPAD_MEMORY_SUBTRACT,
- KEYBOARD_KEYBOARDPAD_MEMORY_MULTIPLY,
- KEYBOARD_KEYBOARDPAD_MEMORY_DIVIDE,
- KEYBOARD_KEYBOARDPAD_PLUSMINUS,
- KEYBOARD_KEYBOARDPAD_CLEAR,
- KEYBOARD_KEYBOARDPAD_CLEAR_ENTRY,
- KEYBOARD_KEYBOARDPAD_BINARY,
- KEYBOARD_KEYBOARDPAD_OCTAL,
- KEYBOARD_KEYBOARDPAD_DECIMAL,
- KEYBOARD_KEYBOARDPAD_HEXADECIMAL,
- KEYBOARD_RESERVED12,
- KEYBOARD_RESERVED13,
- KEYBOARD_LEFTCONTROL,
- KEYBOARD_LEFTSHIFT,
- KEYBOARD_LEFTALT,
- KEYBOARD_LEFT_GUI,
- KEYBOARD_RIGHTCONTROL,
- KEYBOARD_RIGHTSHIFT,
- KEYBOARD_RIGHTALT,
- KEYBOARD_RIGHT_GUI,
- } USBH_HID_KEYBOARD_VALUE_T;
将遥控器的数字键按照原本的数字输入,方向键控制光标方向,OK键回车,*键退格,#键del
- uint8_t IRCode2Key(uint8_t ircode)
- {
- switch(ircode)
- {
- case 0x45:
- return KEYBOARD_1_EXCLAMATION;
- case 0x46:
- return KEYBOARD_2_AT;
- case 0x47:
- return KEYBOARD_3_NUMBER_SIGN;
- case 0x44:
- return KEYBOARD_4_DOLLAR;
- case 0x40:
- return KEYBOARD_5_PERCENT;
- case 0x43:
- return KEYBOARD_6_CARET;
- case 0x07:
- return KEYBOARD_7_AMPERSAND;
- case 0x15:
- return KEYBOARD_8_ASTERISK;
- case 0x9:
- return KEYBOARD_9_OPARENTHESIS;
- case 0x16:
- return KEYBOARD_BACKSPACE;
- case 0x19:
- return KEYBOARD_0_CPARENTHESIS;
- case 0x0D:
- return KEYBOARD_DELETE;
- case 0x0C:
- break;
- case 0x18:
- return KEYBOARD_UPARROW;
- case 0x5E:
- break;
- case 0x08:
- return KEYBOARD_LEFTARROW;
- case 0x1C:
- return KEYBOARD_ENTER;
- case 0x5A:
- return KEYBOARD_RIGHTARROW;
- case 0x42:
- break;
- case 0x52:
- return KEYBOARD_DOWNARROW;
- case 0x4A:
- break;
- default:
- break;
- }
- return 0;
- }
发送按键数据后还要发送一个空的包,表示按键抬起,不然会一直输入,在定时器哪里增加个变量来计时
- uint16_t release_key_time = 0;
- void IRTimeCount(uint32_t us)
- {
- IR_time_us += us;
- if(release_key_time > us)
- {
- release_key_time -= us;
- }
- else if(release_key_time > 0)
- {
- release_key_time = 1;
- }
- }
- void IRAPP(void)
- {
- uint8_t HID_Buffer[8] = {0};
- if(ir_pin_it_flag != 0)
- {
- ir_pin_it_flag = 0;
- IRIOcheck(!LL_GPIO_IsInputPinSet(IR_PIN_GPIO_Port,IR_PIN_Pin));
- }
- if(IR_code_flag == IR_CODE_FLAG_CMD)
- {
- //printf("rev ir data: %02X%02X%02X%02X\r\n",IR_code[0],IR_code[1],IR_code[2],IR_code[3]);
- IR_code_flag = IR_CODE_FLAG_NONE;
- if((IR_code[2]^IR_code[3]) == 0xFF)
- {
- HID_Buffer[2] = IRCode2Key(IR_code[2]);
- USBD_HID_SendReport(&hUsbDeviceFS, HID_Buffer, 8);
- release_key_time = 20000;
- }
- }
- else if(IR_code_flag == IR_CODE_FLAG_REPEAT)
- {
- //printf("rev ir repeat\r\n");
- IR_code_flag = IR_CODE_FLAG_NONE;
- }
- if(release_key_time == 1)
- {
- release_key_time = 0;
- USBD_HID_SendReport(&hUsbDeviceFS, HID_Buffer, 8);
- }
- }
需要注意开启USB后要注释掉串口相关操作,不然会有问题,我这里会出现发送空指令变成输入e的情况,最终效果如下
就实际使用来说蓝牙或者2.4G的遥控更好用,这个项目适合对红外解码和USB通讯的学习。