背景介绍
现在的电脑外设制造商现在接连不断的推出带屏幕的一些外设,主要面向给极客玩家、DIY 玩家,装扮自己的电脑主机和桌面,置于显示屏下方的小监控屏就是其中很受欢迎的一类产品,但存在些许不足之处,分析如下:
该场景下的“监控小屏”类外设当前主要有以下三种方案:
序号 | 实现方式 | 成本 | 性能 | 操作 |
---|---|---|---|---|
1 | 通过 AIDA64 获取电脑的状态信息,需要Wi-Fi支持 | 一般 | 一般 | 较麻烦 |
2 | USB 一线通,使用MCU驱屏,类串口屏思路实现 | 一般 | 较低 | 一般 |
3 | 使用 HDMI 屏幕,直接作为小拓展屏 | 高 | 高 | 简单 |
以上三种方案或成本较高、或性能局限、或操作麻烦,针对上述问题,本创意首次提出了如下方案:
- 使用 USB HS 转 SPI、I2C、UART专用芯片搭配 IO 拓展芯片实现高速驱屏、电容触摸。
- 使用 I2C 环境传感器获取当前室内温湿度,可拓展工作台温湿度监控等功能。
- 直接使用电脑运行 LVGL,无 Flash 限制!无 RAM 限制!搭配 LVGL 设计器实现炫酷界面!
- 通过 WMI 等方式直接读取电脑状态,无需反复配网、配置,直接运行 exe 完成操作。
依靠上述方案实现了一个低成本、高性能、易操作的监控小屏项目。具体实现且听我娓娓道来。。
硬件设计
器件列表
器件 | 功能 |
---|---|
HS3001 | 温湿度传感器 |
STM32G030F6P6 | UART 转 PWM、GPIO 拓展功能 |
CH347T | USB HS 转高速 SPI、I2C、UART 专用芯片 |
触摸屏 | 屏幕为 ST7789 驱动芯片,TOUCH 为 FT6236 |
硬件设计框图
方案介绍如下:
- 通过 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,图标不做商业用途,整体设计界面如下:
主程序的代码量比较大,
实物展示
PCBA,和2.4 寸的触摸屏铁框完美契合
装上外壳,完美贴合
放在桌面上
和小米温湿度计的合影