少女祈祷中...

带显示界面的无线手柄

项目简介

项目设计

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)
{
// if(disp_flush_enabled) {
// /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/

// int32_t x;
// int32_t y;
// for(y = area->y1; y <= area->y2; y++) {
// for(x = area->x1; x <= area->x2; x++) {
// /*Put a pixel to the display. For example:*/
// /*put_px(x, y, *color_p)*/
//
// color_p++;
// }
// }
// }

my_disp_flush(area,color_p);
while(!dma_end)
;
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}
//my_disp_flush在Lcd_Driver.c文件中
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]; // 16-bit color needs 2 bytes per pixel

// 将LVGL颜色缓冲区转换为SPI传输格式
for (uint32_t i = 0; i < buffer_size; i++) {
spi_buffer[i * 2] = color_p[i].full >> 8; // 高8位
spi_buffer[i * 2 + 1] = color_p[i].full & 0xFF; // 低8位
}

#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) {}; // 等待DMA完成
#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)
{
//创建页面栈信号量
//xSemaphore = xSemaphoreCreateMutex();
stack_top = -1;
// memset(page_stack, 0, sizeof(page_stack));
}

每当进入一个新页面时执行入栈操作。

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(xSemaphoreTake(xSemaphore, portMAX_DELAY) != pdTRUE)
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)
{
//释放页面栈信号量
//xSemaphoreGive(xSemaphore);
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);
}

//xSemaphoreGive(xSemaphore);
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(xSemaphoreTake(xSemaphore, portMAX_DELAY) != pdTRUE)


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);

//释放页面栈信号量
//xSemaphoreGive(xSemaphore);

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
/**
* @brief 当前页面改变选择按钮函数
* @param NULL
* @retval NULL
*/
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);
}

/**
* @brief 当前页面 确定选择按钮函数
* @param NULL
* @retval NULL
*/
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();
//参数dirt是确定按钮切换方向
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
/**
* @brief 初始化页面栈
* @param NULL
* @retval NULL
*/
void PageStackInit(void)
{
//创建页面栈信号量
//xSemaphore = xSemaphoreCreateMutex();
stack_top = -1;
// memset(page_stack, 0, sizeof(page_stack));
}
/**
* @brief 获取当前页面名
* @param NULL
* @retval 页面名
*/
PageName getCurPageName(void)
{
if(stack_top < 0)
{
return NonePage;
}
return page_stack[stack_top].page_name;
}
/**
* @brief 获取当前页面对象
* @param NULL
* @retval 页面对象
*/
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;
}

//获取摇杆方向以及程度,将摇杆值转换成 -value~value中
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驱动移植以及通讯实现

接收器开发