【FastBond第三季】挑战部分-基于USB HS的一线通高刷监控副屏设计

背景介绍

现在的电脑外设制造商现在接连不断的推出带屏幕的一些外设,主要面向给极客玩家、DIY 玩家,装扮自己的电脑主机和桌面,置于显示屏下方的小监控屏就是其中很受欢迎的一类产品,但存在些许不足之处,分析如下:

该场景下的“监控小屏”类外设当前主要有以下三种方案:

以上三种方案或成本较高、或性能局限、或操作麻烦,针对上述问题,本创意首次提出了如下方案:

  • 使用 USB HS 转 SPI、I2C、UART专用芯片搭配 IO 拓展芯片实现高速驱屏、电容触摸。

  • 使用 I2C 环境传感器获取当前室内温湿度,可拓展工作台温湿度监控等功能。

  • 直接使用电脑运行 LVGL,无 Flash 限制!无 RAM 限制!搭配 LVGL 设计器实现炫酷界面!

  • 通过 WMI 等方式直接读取电脑状态,无需反复配网、配置,直接运行 exe 完成操作。

依靠上述方案实现了一个低成本、高性能、易操作的监控小屏项目。具体实现且听我娓娓道来。。

硬件设计

器件列表

硬件设计框图

方案介绍如下:

  • 通过 USB 转高速 SPI(60Mbps)驱动 SPI 小屏幕显示,实现高刷新率

  • 通过 USB 转 I2C 读取 TOUCH 芯片、温湿度芯片实现触摸、监测环境温湿度的功能

  • 通过 USB 转高速 UART(6Mbps)和STM32通讯,实现IO、PWM拓展功能

IO 拓展器硬件设计

对于 STM32 端的 IO 分配使用 CubeMX 来完成分配,如下所示:

  • PA13、PA14 作 SWD 烧录引脚

  • PA0、PA1、PA4、PA5 作为 GPIO 输出引脚、PA6、PA7、PA11、PA12 作为 GPIO 输入引脚

  • PB1 作为 PWM 输出引脚、频率为 10KHz

  • PA2、PA3 作为 USART1 通信引脚与 CH347T 通讯

原理图设计

原理图采用 KiCAD 进行设计,CH347T、FPC 等部分封装为手动创建,非系统原理图库。

原理图中,主要分为

  • Type-C 及供电电路

  • CH347T 与 STM32 通过 UART 连接

  • CH347T 与 HS3001 通过 I2C 连接

  • 触摸屏通过 SPI、I2C、GPIO、PWM 与 CH347T 和 STM32 连接

  • PMOS 控制 STM32 电源电路——当 CH347T 建立 USB 连接后 ACT 拉低,STM32 工作;断开连接(但不断电)后 ACT 拉高,STM32 不工作,LCD_BL 拉低,实现了电脑休眠时自动熄灭屏幕的功能

PCB 设计

PCB 当然也是用的 KiCAD 进行的设计,采用双层板设计

外壳设计

同时也设计了一个简单的外壳,HS3001 位置留了缝隙以供空气传递,外壳和屏幕全贴合,并且留有 Type-C 的接口槽孔。

软件设计

IO 拓展器软件设计

modbus 是工业中常用的一种标准的通信协议,有二进制变量(线圈、离散量)和双字节变量(输入寄存器、保持寄存器)四种类型,在工业中广泛用于 IO 控制、数据同步等许多应用,在本项目中的 IO 操作、PWM 占空比设置十分合适,上下位机的代码也可以利用开源库,易于实现。

IO 拓展器采用 modbus 协议与 PC 端软件通讯,来完成对 MCU 的 IO 输入输出、PWM 占空比进行读写操作,本项目是在 STM32G030F6P6 端移植了 FreeModbus 的协议栈以实现 PC 和 STM32 的通讯,由于篇幅原因,对于移植过程不做赘述,核心的代码如下:

    // 使能 modbus 需要的 RS485 串口和定时器
    eMBInit(MB_RTU, id, &huart2, baud, &htim17);
    // 使能 modbus 协议栈
    eMBEnable();
    // 使能 PWM
    HAL_TIM_PWM_Start(&htim14, TIM_CHANNEL_1);              
    while (1)
    {                                    
        // modbus 轮训
        eMBPoll();
        // 如果被主机读
        if (usSRegInRead) {
            usSRegInRead = 0;
        }
        // 如果被主机写
        if (usSRegHoldWrite) {
            HAL_GPIO_WritePin(OUT0_GPIO_Port, OUT0_Pin, IO_OUT0 ? GPIO_PIN_SET : GPIO_PIN_RESET);
            HAL_GPIO_WritePin(OUT1_GPIO_Port, OUT1_Pin, IO_OUT1 ? GPIO_PIN_SET : GPIO_PIN_RESET);
            HAL_GPIO_WritePin(OUT2_GPIO_Port, OUT2_Pin, IO_OUT2 ? GPIO_PIN_SET : GPIO_PIN_RESET);
            HAL_GPIO_WritePin(OUT3_GPIO_Port, OUT3_Pin, IO_OUT3 ? GPIO_PIN_SET : GPIO_PIN_RESET);
            // PWM 占空比
            __HAL_TIM_SetCompare(&htim14, TIM_CHANNEL_1, PWM_DUTY);                                                                                              
            usSRegHoldWrite = 0;
        }                                                                                                                                     
        IO_IN0 = (USHORT)HAL_GPIO_ReadPin(IN0_GPIO_Port, IN0_Pin);
        IO_IN1 = (USHORT)HAL_GPIO_ReadPin(IN1_GPIO_Port, IN1_Pin);
        IO_IN2 = (USHORT)HAL_GPIO_ReadPin(IN2_GPIO_Port, IN2_Pin);
        IO_IN3 = (USHORT)HAL_GPIO_ReadPin(IN3_GPIO_Port, IN3_Pin);                                                                                                                                                     
    }

PC 端软件设计

IO 拓展器通讯 API

与 IO 拓展器的通讯使用 libmodbus 来实现,对于读 IO 输入对应为 modbus 读输入寄存器操作,写 IO 输出和 PWM 占空比对应写保持寄存器操作,具体代码如下

#include "driver.h"
#include <modbus.h>

modbus_t* g_ctx;
char g_comx[1024];

void exio_set_com_global(const char* comx) {
        strcpy(g_comx, comx);
}

static int exio_open(void) {
        int addr = 1;
        g_ctx = modbus_new_rtu(g_comx, 115200, 'N', 8, 1);
        if (g_ctx == NULL) {
                fprintf(stderr, "Unable to create the libmodbus context\n");
                return -1;
        }
        modbus_set_slave(g_ctx, addr);
        if (modbus_connect(g_ctx) == -1) {
                fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno));
                modbus_free(g_ctx);
                return -1;
        }
        return 0;
}
static void exio_close(void) {
        modbus_close(g_ctx);
        modbus_free(g_ctx);
}
int exio_set_out(int index, uint16_t value) {
        int rc;
        rc = exio_open();
        if (rc == -1) {        
               return -1;
        }
        rc = modbus_write_register(g_ctx, index, value);
        if (rc == -1) {
                fprintf(stderr, "Failed to write holding register: %s\n", modbus_strerror(errno));
                exio_close();
                return -1;
        }
        exio_close();
        return 0;
}
int exio_get_in(int index, uint16_t* p_value) {
        int nb = 1;
        int rc;
        rc = exio_open();
        if (rc == -1) {
                exio_close();
                return -1;
        }
        rc = modbus_read_input_registers(g_ctx, index, nb, p_value);
        if (rc == -1) {
                fprintf(stderr, "Failed to read input registers: %s\n", modbus_strerror(errno));
                exio_close();
                return -1;
        }
        exio_close();
        return 0;
}

SPI、I2C 通讯 API

对于 SPI、I2C 的通讯,沁恒已经封装成了 DLL 库,只需调用即可,但仍较为复杂,本项目对其进行了一些简化封装,具体代码比较繁多,具体代码可见附件。

驱动封装

对于触摸屏的驱动芯片,也就是 ST7789,采用四线 SPI 通讯方式,其中 D/CX 引脚为 IO 拓展器的 IO 输出脚,用前文的 libmodbus 进行操作,其他的 SCL、SDA、CSX 为 SPI 标准信号线,使用 CH347T 库进行操作,搭配操作实现高速刷屏,代码较为繁琐,可以查看附件

对于触摸屏的 FT6236 触摸芯片和 HS3001 温湿度传感器,都采用 I2C 的通讯方式,使用 CH347T 库进行操作,这里给出 HS3001 读取温湿度的例子

从手册中可以看到 HS3001 的 I2C 8-bit 的地址为 0x88,读取温湿度数据时需要先写一个 0x00 唤醒 HS3001 请求测量,然后再读取温湿度数据,代码如下

void HS3003_Read(float* t, float* h) {
        uint8_t wr_buf[4] = { 0X88, 0, 0, 0, };
        uint8_t rd_buf[4] = { 0, 0, 0, 0, };
        uint16_t humi, temp;

        i2c_writeread(wr_buf, 2, rd_buf, 0);
        Sleep(50);
        wr_buf[0] = 0x89;
        i2c_writeread(wr_buf, 1, rd_buf, 4);
        Sleep(200);

        if ((rd_buf[0] & HS3003_MASK_STATUS) != HS3003_STATUS_VALID) {
                printf("timeout\r\n");
        }
        humi = rd_buf[0];
        humi <<= 8;
        humi |= rd_buf[1];
        humi &= HS3003_MASK_HUMI;

        temp = rd_buf[2];
        temp <<= 8;
        humi |= rd_buf[3];
        temp &= HS3003_MASK_TEMP;
        temp >>= 2;

        *h= HS3003_CALC_HUMI(humi);
        *t= HS3003_CALC_TEMP(temp);          
}

对于 PC 的状态信息读取采用 Windows 给出的一些 API 和开源的一些库代码来完成,目前支持了 CPU 温度/占用率、GPU 温度/占用率、主板温度、内存占用率这几个信息的读取,代码比较繁多,具体代码细节可以查看附件。

至此,完成了所有硬件信息的读写,然后就是如何将其展示出来并且可供用户操作了,本项目采用了现在最流行的 LVGL 来作为界面库使用。

对接 LVGL 驱动框架

对于 LVGL 的移植,主要包含有显示驱动接口和输入驱动(在此为触摸)接口。

对于显示驱动接口,本质就是一个对于显存的操作,往显存中填颜色,然后使用 SPI 全局刷新到屏幕上面,具体接口代码如下:

static void lcd_refresh(void) {
        spi_write16b_stream((uint16_t*)clr, LCD_H * LCD_W * 2, LCD_W * 2);
}

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
        if(disp_flush_enabled) {
                int32_t x;
                int32_t y;
                for(y = area->y1; y <= area->y2; y++) {
                        for(x = area->x1; x <= area->x2; x++) {
                                clr[y][x] = color_p->full;
                                color_p++;
                        }
                }
                lcd_refresh();
        }
        lv_disp_flush_ready(disp_drv);
}

对于输入驱动接口,本质就是 xy 坐标和一个按下状态标志位,读取一下 FT6236 的寄存器即可

static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
        static lv_coord_t last_x = 0;
        static lv_coord_t last_y = 0;
        FT6236_Scan(&touch_x, &touch_y, &touch_sta);
        if(touchpad_is_pressed()) {
                touchpad_get_xy(&last_x, &last_y);
                data->state = LV_INDEV_STATE_PR;
        }
        else {
                data->state = LV_INDEV_STATE_REL;
        }
        data->point.x = last_x;
        data->point.y = last_y;
}

LVGL 界面设计

界面设计是使用的 GUI Guider 进行的设计,图标来自 iconfont,图标不做商业用途,整体设计界面如下:

主程序的代码量比较大,

实物展示

和小米温湿度计的合影:

代码和PCB设计文件下载:基于USB HS的一线通高刷监控副屏设计

1 个赞