瑞萨RA6M5作为一款高性能ARM Cortex-M4内核的MCU,其丰富的外设资源使其非常适合嵌入式图形界面开发。而ST7735是一款常见的1.8英寸TFT液晶驱动芯片,支持262K色显示。这对组合在智能家居控制面板、工业HMI等场景中非常实用。
硬件连接上,RA6M5通过GPIO模拟SPI接口与ST7735通信,这种软件SPI方案相比硬件SPI更加灵活。实际接线时需要注意:
提示:软件SPI的引脚可以任意配置,但建议选择同一端口组的GPIO以提高操作效率。比如RA6M5的P0组GPIO可以通过位带操作实现快速翻转。
软件SPI的核心是通过GPIO电平变化模拟SPI时序。以发送一个字节为例:
c复制void SendDataSPI(uint8_t dat) {
for(uint8_t i=0; i<8; i++) {
if(dat & 0x80) GPIO_SET(MOSI_PIN);
else GPIO_CLR(MOSI_PIN);
dat <<= 1;
GPIO_CLR(SCK_PIN); // 下降沿采样
GPIO_SET(SCK_PIN);
}
}
这里有几个关键点:
实测在RA6M5运行120MHz时,软件SPI可以达到约2MHz的时钟频率,完全满足ST7735的驱动需求。
ST7735需要一系列初始化命令才能正常工作,主要包括:
典型的初始化代码片段:
c复制void LCD_Init(void) {
// 硬件复位
OLED_RST_Clr();
DelayMs(100);
OLED_RST_Set();
DelayMs(200);
// 发送初始化命令序列
WriteComm(0x11); // 退出睡眠
DelayMs(120);
WriteComm(0xB1); // 帧率控制
WriteData(0x05);
WriteData(0x3C);
// ...更多配置命令
WriteComm(0x29); // 开启显示
}
字符显示本质是将字模数据写入显存。我们首先需要定义字体数据结构:
c复制typedef struct {
uint8_t width; // 字符宽度
uint8_t height; // 字符高度
const uint16_t *data; // 字模数据指针
} FontDef;
以7x10字体为例,每个字符用70个bit表示(7宽x10高),存储为16位数组。显示时逐像素判断是否需要绘制:
c复制void DrawChar(uint16_t x, uint16_t y, char ch, FontDef font, uint16_t color) {
uint32_t b = font.data[(ch-32)*font.height]; // 获取字模数据
for(uint8_t j=0; j<font.width; j++) {
if((b << j) & 0x8000) {
DrawPixel(x+j, y, color); // 绘制前景色像素
}
}
}
显示数字变量需要先将其转换为字符串。一个优化的实现方式:
c复制void ShowIntNum(uint16_t x, uint16_t y, int num, FontDef font, uint16_t color) {
char str[10];
int i = 0;
if(num == 0) str[i++] = '0';
else while(num > 0) {
str[i++] = num % 10 + '0';
num /= 10;
}
// 反转字符串
for(int j=0; j<i/2; j++) {
char temp = str[j];
str[j] = str[i-1-j];
str[i-1-j] = temp;
}
str[i] = '\0';
DrawString(x, y, str, font, color);
}
直线绘制采用Bresenham算法,避免浮点运算:
c复制void DrawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) {
int16_t dx = abs(x1-x0), sx = x0<x1 ? 1 : -1;
int16_t dy = -abs(y1-y0), sy = y0<y1 ? 1 : -1;
int16_t err = dx+dy, e2;
while(1) {
DrawPixel(x0, y0, color);
if(x0==x1 && y0==y1) break;
e2 = 2*err;
if(e2 >= dy) { err += dy; x0 += sx; }
if(e2 <= dx) { err += dx; y0 += sy; }
}
}
圆形绘制同样采用中点圆算法,只需整数运算即可实现平滑圆边。
显示位图时,直接使用DMA传输可以大幅提高效率。RA6M5的DMA控制器可以配置为:
c复制void DMA_Config(void) {
dma_instance.p_api->open(dma_instance.p_ctrl, dma_instance.p_cfg);
dma_instance.p_api->enable(dma_instance.p_ctrl);
}
void ShowImage_DMA(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *img) {
SetWindow(x, y, x+w-1, y+h-1); // 设置显示区域
DC_Data();
CS_Low();
dma_instance.p_api->transfer(dma_instance.p_ctrl, img, NULL, w*h*2);
while(dma_busy); // 等待传输完成
CS_High();
}
一个简单的按钮控件可以这样定义:
c复制typedef struct {
uint16_t x, y, width, height;
char *text;
void (*onClick)(void);
uint16_t color, bgColor;
} Button;
void DrawButton(Button *btn) {
// 绘制背景
FillRect(btn->x, btn->y, btn->width, btn->height, btn->bgColor);
// 绘制边框
DrawRect(btn->x, btn->y, btn->width, btn->height, btn->color);
// 绘制文字(居中)
uint16_t textX = btn->x + (btn->width - strlen(btn->text)*8)/2;
uint16_t textY = btn->y + (btn->height - 16)/2;
DrawString(textX, textY, btn->text, Font_8x16, btn->color, btn->bgColor);
}
采用状态机模式管理不同界面:
c复制typedef enum {
PAGE_HOME,
PAGE_SETTINGS,
PAGE_ABOUT
} PageType;
typedef struct {
void (*init)(void);
void (*draw)(void);
void (*handleTouch)(uint16_t x, uint16_t y);
} Page;
Page currentPage;
void ChangePage(PageType newPage) {
switch(newPage) {
case PAGE_HOME:
currentPage.init = Home_Init;
currentPage.draw = Home_Draw;
break;
// 其他页面初始化
}
currentPage.init();
currentPage.draw();
}
只刷新变化区域可以显著提高性能:
c复制void UpdateArea(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
SetWindow(x1, y1, x2, y2);
CS_Low();
for(uint16_t y = y1; y <= y2; y++) {
for(uint16_t x = x1; x <= x2; x++) {
uint16_t color = GetPixelColor(x, y);
WriteData(color >> 8);
WriteData(color & 0xFF);
}
}
CS_High();
}
在RAM中创建虚拟屏幕缓冲区,完成所有绘制操作后一次性刷新到实际屏幕:
c复制uint16_t frameBuffer[128][160]; // 匹配屏幕分辨率
void FlushFrameBuffer(void) {
SetWindow(0, 0, 127, 159);
CS_Low();
for(uint16_t y = 0; y < 160; y++) {
for(uint16_t x = 0; x < 128; x++) {
uint16_t color = frameBuffer[x][y];
WriteData(color >> 8);
WriteData(color & 0xFF);
}
}
CS_High();
}
基于这套图形库,我们可以实现一个简单的家居控制界面:
c复制void Home_Draw(void) {
// 绘制背景
FillScreen(RGB565(240, 240, 240));
// 显示标题
DrawString(10, 5, "智能家居控制", Font_11x18, BLACK, TRANSPARENT);
// 显示温湿度
char tempStr[20];
sprintf(tempStr, "温度: %.1f℃", getTemperature());
DrawString(20, 40, tempStr, Font_8x16, BLUE, TRANSPARENT);
// 绘制灯光开关按钮
Button lightBtn = {50, 80, 80, 40, "灯光", ToggleLight, WHITE, BLUE};
DrawButton(&lightBtn);
}
工业场景下需要更健壮的界面设计:
c复制void DrawPressureChart(uint16_t *values, uint8_t count) {
uint16_t maxVal = 0;
for(uint8_t i=0; i<count; i++)
if(values[i] > maxVal) maxVal = values[i];
// 绘制坐标轴
DrawLine(30, 30, 30, 130, BLACK);
DrawLine(30, 130, 200, 130, BLACK);
// 绘制数据曲线
for(uint8_t i=1; i<count; i++) {
uint16_t y1 = 130 - (values[i-1]*100/maxVal);
uint16_t y2 = 130 - (values[i]*100/maxVal);
DrawLine(30+i*5, y1, 30+(i+1)*5, y2, RED);
}
}