带显示界面的无线手柄 项目简介 项目设计 LCD驱动移植 参考链接 ,注意SPI传输速率不能超过ST7735。
freeRTOS移植 参考链接 。
LVGL移植 参考链接 。 刷新函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include "mytask.h" extern SemaphoreHandle_t xLCD_DMA_Semaphore;extern uint8_t dma_end;static void disp_flush (lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { my_disp_flush(area,color_p); while (!dma_end) ; lv_disp_flush_ready(disp_drv); } void my_disp_flush (const lv_area_t *area, lv_color_t *color_p) { uint16_t w = area->x2 - area->x1 + 1 ; uint16_t h = area->y2 - area->y1 + 1 ; if ((w == 0 ) || (h == 0 )) return ; TFT_CS_L(); ST7735_SetAddressWindow(area->x1, area->y1, area->x2, area->y2); TFT_DC_D(); uint16_t buffer_size = w * h; uint8_t spi_buffer[buffer_size * 2 ]; for (uint32_t i = 0 ; i < buffer_size; i++) { spi_buffer[i * 2 ] = color_p[i].full >> 8 ; spi_buffer[i * 2 + 1 ] = color_p[i].full & 0xFF ; } #ifdef USE_SPI_DMA HAL_SPI_Transmit_DMA(&ST7735_SPI_PORT, spi_buffer, sizeof (spi_buffer)); while (ST7735_SPI_PORT.State == HAL_SPI_STATE_BUSY_TX) {}; #else HAL_SPI_Transmit(&ST7735_SPI_PORT, spi_buffer, sizeof (spi_buffer), HAL_MAX_DELAY); #endif TFT_CS_H(); }
LVGL 页面管理器开发 页面管理器采用栈结构,其中页面结构体设计如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 typedef enum { NonePage, MainPage, SettingPage, }PageName; typedef void (*PageCreateCallback) (lv_obj_t * curPage,void * data) ;typedef struct { PageCreateCallback create_cb; lv_obj_t * page; int enter_anim; int exit_anim; PageName page_name; void * data; }PageStackItem;
首先静态创建页面栈PageStackItem page_stack[MAX_STACK_DEPTH]
,并将页面栈初始化。
1 2 3 4 5 6 7 8 void PageStackInit (void ) { stack_top = -1 ; }
每当进入一个新页面时执行入栈操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 bool PageStackPush (int enter_anim,int exit_anim,PageCreateCallback createpage_cb,PageName page_name,void * data) { if (stack_top >= MAX_STACK_DEPTH - 1 )return false ; stack_top++; page_stack[stack_top].create_cb = createpage_cb; page_stack[stack_top].page = lv_obj_create(NULL ); if (enter_anim == 0 ) page_stack[stack_top].enter_anim = LV_SCR_LOAD_ANIM_FADE_ON; else page_stack[stack_top].enter_anim = enter_anim; if (exit_anim == 0 ) page_stack[stack_top].exit_anim = LV_SCR_LOAD_ANIM_OVER_BOTTOM; else page_stack[stack_top].exit_anim = exit_anim; page_stack[stack_top].page_name = page_name; page_stack[stack_top].data = data; if (page_stack[stack_top].page == NULL ) { stack_top--; return false ; } createpage_cb(page_stack[stack_top].page,data); if (stack_top != 0 ) { lv_scr_load_anim(page_stack[stack_top].page, page_stack[stack_top].enter_anim, 200 , 0 , true ); } else { lv_scr_load(page_stack[stack_top].page); } return true ; }
每当返回上一个页面时执行出栈操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bool PageStackPop (void ) { if (stack_top <= 0 )return false ; stack_top--; page_stack[stack_top].page = lv_obj_create(NULL ); page_stack[stack_top].create_cb(page_stack[stack_top].page,page_stack[stack_top].data); lv_scr_load_anim(page_stack[stack_top].page, page_stack[stack_top+1 ].exit_anim, 200 , 0 , true ); return true ; }
每个页面的所有按钮都是由一个静态数组存储,由于LCD屏幕没有触摸功能,也没有移植lvgl 输入设备,所以选择了使用自定义函数来控制这些按键选择 和按下。每个页面都必须实现按钮选择函数和按钮确认函数,如Main页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void select_btn_Main (int value) { lv_obj_t * cur_Page = getCurPage(); lv_obj_set_user_data(cur_Page, (void *)(intptr_t )(value)); lv_event_send(cur_Page, LV_EVENT_KEY, NULL ); uart_debug_print("select btn " ,select_btn); } void btns_Main (void ) { lv_event_send(main_btns[select_btn], LV_EVENT_CLICKED,NULL ); }
它们将由以下两个函数统一调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void change_select_btn (int dirt) { PageName curpage = getCurPageName(); if (curpage == MainPage) select_btn_Main(dirt); else if (curpage == SettingPage) select_btn_Set(dirt);; } void click_btn (void ) { PageName curpage = getCurPageName(); if (curpage == MainPage) btns_Main(); else if (curpage == SettingPage) btns_Set(); }
这样只需要在合适的地方调用change_select_btn和click_btn就能实现按钮的切换和点击。 页面管理器其它函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void PageStackInit (void ) { stack_top = -1 ; } PageName getCurPageName (void ) { if (stack_top < 0 ) { return NonePage; } return page_stack[stack_top].page_name; } lv_obj_t * getCurPage (void ) { if (stack_top < 0 ) { return NULL ; } return page_stack[stack_top].page; }
LVGL 页面设计 目前只有两个页面:MainPage和SettingPage。 ManPage为6个圆形按钮组成正六边形以及中间一个按钮,每次切换按钮时都会进行一次轮换当前按钮会向下一个按钮的位置移动,按钮动画以及更新函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 static void update_button_layout (void ) { for (int i = 0 ; i < Main_BTNS_CNT; i++) { int x = layout_xy[(btn_changeIdx + i) % Main_BTNS_CNT][0 ]+5 ; int y = layout_xy[(btn_changeIdx + i) % Main_BTNS_CNT][1 ]+10 ; if (i == select_btn) { lv_anim_t anim_size; lv_anim_init(&anim_size); lv_anim_set_var(&anim_size, main_btns[i]); lv_anim_set_time(&anim_size, 200 ); lv_anim_set_values(&anim_size, lv_obj_get_width(main_btns[i]), 30 ); lv_anim_set_exec_cb(&anim_size, (lv_anim_exec_xcb_t )lv_obj_set_width); lv_anim_start(&anim_size); }else { lv_anim_t anim_size; lv_anim_init(&anim_size); lv_anim_set_var(&anim_size, main_btns[i]); lv_anim_set_time(&anim_size, 200 ); lv_anim_set_values(&anim_size, lv_obj_get_width(main_btns[i]), 24 ); lv_anim_set_exec_cb(&anim_size, (lv_anim_exec_xcb_t )lv_obj_set_width); lv_anim_start(&anim_size); } lv_anim_t anim_pos; lv_anim_init(&anim_pos); lv_anim_set_var(&anim_pos, main_btns[i]); lv_anim_set_time(&anim_pos, anim_time); lv_anim_set_values(&anim_pos, lv_obj_get_x(main_btns[i]), x); lv_anim_set_exec_cb(&anim_pos, (lv_anim_exec_xcb_t )lv_obj_set_x); lv_anim_start(&anim_pos); lv_anim_init(&anim_pos); lv_anim_set_var(&anim_pos, main_btns[i]); lv_anim_set_time(&anim_pos, anim_time); lv_anim_set_values(&anim_pos, lv_obj_get_y(main_btns[i]), y); lv_anim_set_exec_cb(&anim_pos, (lv_anim_exec_xcb_t )lv_obj_set_y); lv_anim_start(&anim_pos); } }
当某个按钮被选中时就会移动到中间并放大,当确认按键被点击时,就会触发该按钮的事件回调函数。
摇杆开发 摇杆模块由xy轴adc输出和摇杆按键构成,其中adc电压范围是05v,因此不能使用STM32直接测量,这里采用两个10K欧的电阻将adc变成02.5v。 判断摇杆移动逻辑:首先进行摇杆校准,读取并存储摇杆xy轴不移动时adc的值并保存。然后控制摇杆绕最大的圈,以此获得摇杆xy轴最大/小adc的值。 然后将摇杆进行类似’归一化’的操作,将摇杆xy轴转换成 -value~value
的范围内(注意由于模块实际上x正半轴和负半轴范围不一样,因此应该采取一定的比例将正负半轴平衡)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 int lever_init (void ) { if (key_ok == 2 )return key_ok; if (HAL_GPIO_ReadPin(H_key_GPIO_Port,H_key_Pin)==GPIO_PIN_RESET) { vTaskDelay(20 ); if (HAL_GPIO_ReadPin(H_key_GPIO_Port,H_key_Pin)==GPIO_PIN_RESET) key_ok++; } HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_handlexy,2 ); if (key_ok == 0 ) { HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_handlexy,2 ); midx = adc_handlexy[0 ]; midy = adc_handlexy[1 ]; minx = maxx = midx; miny = maxy = midy; } else if (key_ok == 1 ) { minx = adc_handlexy[0 ]>minx?minx:adc_handlexy[0 ]; miny = adc_handlexy[1 ]>miny?miny:adc_handlexy[1 ]; maxx = adc_handlexy[0 ]<maxx?maxx:adc_handlexy[0 ]; maxy = adc_handlexy[1 ]<maxy?maxy:adc_handlexy[1 ]; } else uart_debug_print("init ok" ,-1 ); return key_ok; } void get_dir (float * dirt,int value) { update_value(); float cdx = adc_handlexy[0 ] - midx,cdy = adc_handlexy[1 ] - midy; dirt[0 ] = cdx*value/(float )(maxx-minx); dirt[1 ] = cdy*value/(float )(maxy-miny); }
NRF24L01驱动移植以及通讯实现 接收器开发