1. 前言
本节实验是在上节实验1(ADC采集并转换电压值+串口打印输出)的基础上,将采集到的电压直接显示在LCD上。
2. 硬件部分
2.1 LCD简介
液晶显示(LCD)是嵌入式系统中常见的人机交互方式,广泛应用于工业控制、智能家电、医疗设备和消费电子产品。Renesas RA4L1 微控制器(MCU)内置 Segment LCD Controller (SLCDC),可直接驱动 静态、1/2、1/3、1/4 Bias 的段式 LCD 显示屏,无需额外的 LCD 驱动芯片。这种集成方案不仅降低了硬件成本,还简化了设计。
2.2 RA4L1开发板上的LCD模块
本次实验用到了瑞萨开发板的SLCDC模块。RA4L1 的 SLCDC 模块提供了一种高效、低功耗、低成本的 LCD 显示方案。通过 FSP 提供的 r_slcdc 驱动,开发者可以快速初始化 LCD,轻松控制显示内容。在实际项目中,合理配置 COM/SEG 引脚、优化时钟和对比度设置,可以进一步提升 LCD 显示效果。
开发板板载的LCD缩略图如下所示

● SEG(Segment):控制 LCD 具体显示的段,如数码管的 A-G 段。
● COM(Common):LCD 的 公共信号,决定哪个段被驱动,多 COM 允许减少 I/O 引脚数量。
原理图如下,这张图要在后面软件配置引脚时用到

3. 软件部分
复制上次实验的项目 ,修改名称为02_Voltage_DisplayOnLCD
。
3.1 LCD配置
首先配置引脚。在pins
--SLCDC
,选择工作模式4 slice
,也同时配置段所对应的引脚、VL引脚。 记得把CAPH和CAPL也配置上 ,这里配置的P206和P207会和上一次实验的UART冲突,所以我把SCI4换到SCI3了,对应修改UART_SCI3部分属性。



VL(Voltage Level)引脚用于提供 LCD 偏置电压,控制 LCD 的对比度和驱动电压。
在 内部电压提升模式(Internal Voltage Boosting) 下,RA MCU 会自动产生 VL1、VL2、VL3、VL4 并提供给 LCD 作为驱动电压。在 外部电压模式 下,这些电压需要由外部电路提供。

然后配置stacks。New Stack
-- Graphics
-- Segment LCD

双击g_slcdc0 Segment
,点击屏幕下方的属性,按下图配置:

属性解释:
● Name(名称):g_slcdc0 这是 SLCDC 模块的实例名称。
● Source(时钟源):LOCO(Low-Speed On-Chip Oscillator,低速片上振荡器),该选项决定 SLCDC 使用 LOCO 作为时钟来源。
● Divisor(分频器):(LOCO/SOSC) 256 代表 LOCO 或 SOSC(Sub-Oscillator,子振荡器)时钟 除以 256 作为 SLCDC 驱动频率。较大的除数意味着更低的 LCD 刷新速率。
● Bias method(偏置方式):1/3 bias 采用 1/3 偏压驱动,适用于多 COM LCD,可减少功耗并提升对比度。
● Timeslice(时间片):4-slice 说明使用 4 个 COM(公共端),即 四路复用驱动,减少 MCU 需要的 I/O 端口。
● Waveform(波形模式):Waveform A 选择 波形 A,影响 LCD 显示的刷新方式。
● Drive method(驱动方式):Internal voltage boosting 采用 内部电压升压,可以提高 LCD 对比度,无需外部电压源。
● Reference Voltage(参考电压):Select VL1 or VCC 允许选择 VL1 或 VCC 作为 LCD 驱动电压。
● Default contrast(默认对比度):0 如果支持,则默认对比度设置为 0,可能需要在代码中动态调整。
3.2 LCD驱动编写
3.2.1 LCD相关API解释
(1)LCD打开端口函数R_SLCDC_Open
为 LCD 驱动分配控制块 (slcdc_ctrl_t) 以及内部的资源,以确保 SLCDC 可以开始正常运行。 完成初始化后,使 LCD 驱动模块处于待启动状态,等待后续调用 R_SLCDC_Start() 函数真正启动 LCD 输出驱动波形。
(2)LCD启动的函数是R_SLCDC_Start
在调用 R_SLCDC_Open() 完成 SLCDC 初始化后,R_SLCDC_Start() 用于正式启动 LCD 驱动信号输出。调用此函数后,MCU 开始向 LCD 面板输出预配置的驱动波形和电压信号,LCD 显示正式开始工作。
(3)批量写入函数R_SLCDC_Write
数据被写入SLCDC内部的段数据寄存器中,从而控制LCD各段的亮灭状态。
SEG和COM的对应关系如下图所示,之后的驱动编写都参考这张图。


假设我要让第1个数码管显示数字0,0的段码对应的是除了1G=0
外,1A、1B...一直到1F都是1。
段 |
COM3 |
COM2 |
COM1 |
COM0 |
---|
SEG3 |
1 |
1 |
0 |
1 |
SEG11 |
0 |
1 |
1 |
1 |
也就是SEG3写入0xD、SEG11写入0x7。
代码如下
uint8_t segment_data_num1[] = {0x0D,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07};
R_SLCDC_Write(&g_slcdc0_ctrl, 3, segment_data_num1, sizeof(segment_data_num1));
上面表示从SEG3开始,连续写9次数据,写完SEG11结束。但是SEG4~SEG10我们没用上,所以这样写是有多余操作的,不优雅。
(4)单独修改某个段的函数R_SLCDC_Modify
R_SLCDC_Modify() 是用于修改单个段寄存器中的部分位的函数,属于 SLCDC 驱动的精细控制接口。
例如我们要让LCD左上角的小电池亮起来,显示两格电量,需要让SEG2的T1、T3、T4都为1,T2=0,也就是对SEG2写入0xD,掩码为0xF
R_SLCDC_Modify (&g_slcdc0_ctrl, 2, 0xD, 0xF);
实际效果如下

3.2.2 新建lcd.c文件
在src目录下创建lcd
文件夹后,创建lcd.c文件。实现了数字0~9的显示、设置小数点和冒号的显示、电池小图标的显示。
lcd.c文件中的代码如下:
#include "lcd.h"
const uint8_t digit_segments[10] =
{
SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F,
SEG_B | SEG_C,
SEG_A | SEG_B | SEG_G | SEG_E | SEG_D,
SEG_A | SEG_B | SEG_G | SEG_C | SEG_D,
SEG_F | SEG_G | SEG_B | SEG_C,
SEG_A | SEG_F | SEG_G | SEG_C | SEG_D,
SEG_A | SEG_F | SEG_E | SEG_D | SEG_C | SEG_G,
SEG_A | SEG_B | SEG_C,
SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G,
SEG_A | SEG_B | SEG_C | SEG_D | SEG_F | SEG_G
};
const uint8_t segment_com[7] =
{ 0,
1,
2,
3,
2,
0,
1
};
const uint8_t digit_seg_map[6][7] =
{
{ 11, 11, 11, 3, 3, 3, 3 },
{ 16, 16, 16, 15, 15, 15, 15 },
{ 23, 23, 23, 22, 22, 22, 22 },
{ 29, 29, 29, 24, 24, 24, 24 },
{ 39, 39, 39, 30, 30, 30, 30 },
{ 41, 41, 41, 40, 40, 40, 40 },
};
void LCD_Init(void)
{
fsp_err_t err;
err = R_SLCDC_Open (&g_slcdc0_ctrl, &g_slcdc0_cfg);
assert(FSP_SUCCESS == err);
R_BSP_SoftwareDelay (1, BSP_DELAY_UNITS_MILLISECONDS);
err = R_SLCDC_Start (&g_slcdc0_ctrl);
assert(FSP_SUCCESS == err);
R_BSP_SoftwareDelay (1, BSP_DELAY_UNITS_MILLISECONDS);
}
void LCD_ShowDigit(uint8_t position, uint8_t digit)
{
if (position > 5 || digit > 9)
return;
uint8_t segs = digit_segments[digit];
for (int i = 0; i < 7; i++)
{
uint8_t seg_index = digit_seg_map[position][i];
uint8_t com = segment_com[i];
uint8_t bit = 1 << com;
bool on = (segs & (1 << i)) != 0;
uint8_t set = on ? bit : 0;
R_SLCDC_Modify (&g_slcdc0_ctrl, seg_index, set, bit);
}
}
void LCD_ShowNumber(uint32_t number, uint8_t d)
{
for (int i = 0; i < d; i++)
{
uint8_t t = number % 10;
LCD_ShowDigit (5 - i, t);
number /= 10;
}
}
void LCD_setColon(bool on)
{
R_SLCDC_Modify (&g_slcdc0_ctrl, 16, on ? 0x8 : 0x0, 0x8);
}
void LCD_setDot(bool on)
{
R_SLCDC_Modify (&g_slcdc0_ctrl, 29, on ? 0x8 : 0x0, 0x8);
}
void LCD_setBattery(bool on, uint8_t num)
{
if (on)
{
switch (num)
{
case 0:
R_SLCDC_Modify (&g_slcdc0_ctrl, 2, 0x1, 0xF);
break;
case 1:
R_SLCDC_Modify (&g_slcdc0_ctrl, 2, 0x9, 0xF);
break;
case 2:
R_SLCDC_Modify (&g_slcdc0_ctrl, 2, 0xD, 0xF);
break;
case 3:
R_SLCDC_Modify (&g_slcdc0_ctrl, 2, 0xF, 0xF);
break;
}
}
else
{
R_SLCDC_Modify (&g_slcdc0_ctrl, 2, 0x0, 0xF);
}
}
3.2.3 新建lcd.h文件
头文件主要是声明lcd.c中的函数
#ifndef LCD_LCD_H_
#define LCD_LCD_H_
#include "hal_data.h"
#define SEG_A (1 << 0)
#define SEG_B (1 << 1)
#define SEG_C (1 << 2)
#define SEG_D (1 << 3)
#define SEG_E (1 << 4)
#define SEG_F (1 << 5)
#define SEG_G (1 << 6)
void LCD_Init(void);
void LCD_ShowDigit(uint8_t position, uint8_t digit);
void LCD_ShowNumber(uint32_t number, uint8_t d);
void LCD_setColon(bool on);
void LCD_setDot(bool on);
void LCD_setBattery(bool on, uint8_t num);
#endif
3.3 修改hal_entry.c
先在头文件引入lcd.h
#include "lcd/lcd.h"
将在hal_entry函数内改为如下:
void hal_entry(void)
{
double volt;
UART_Init ();
ADC_Init ();
LCD_Init ();
LCD_setDot (true);
while (1)
{
volt = Read_ADC_Voltage_Value ();
uint32_t volt_int = volt * 100;
if (volt_int == 0)
{
LCD_setBattery (true, 0);
}
else if (volt_int >= 1 && volt_int <= 110)
{
LCD_setBattery (true, 1);
}
else if (volt_int >= 111 && volt_int <= 220)
{
LCD_setBattery (true, 2);
}
else
{
LCD_setBattery (true, 3);
}
LCD_ShowNumber (volt_int, 3);
R_BSP_SoftwareDelay (100, BSP_DELAY_UNITS_MILLISECONDS);
}
}
4. 编译下载测试
使用杜邦线连接一个200Ω电阻串联100K的可调电阻,将可调电阻的滑动端接到P500引脚。
下载程序,效果如底部视频所示。