RA4L1设计有段式液晶屏的硬件驱动 SLCDC(Segment LCD Controller),为驱动段式液晶显示器(如常见的数码管、字符段码屏等)设计,可适配不同分辨率的段式屏,并通过内部寄存器直接管理显示数据的刷新,通过硬件逻辑实现对各显示段(Segment)和公共电极(Common)的驱动控制。
一、驱动原理
开发板配套的LCD段码屏管脚定义如下:COMn是公共端口,pin1~13控制显示的段,可以看作表格的行和列,当pin1~13某一个管脚,与COM1~4某一个管脚形成电势差时(有相对压差),其对应的LCD段就会显示出对应的图形;具体某个管脚对应的位置可以查表得到,驱动代码的结构在后续段落详解。
● SEG(Segment):控制 LCD 具体显示的段,如数码管的 A-G 段。
● COM(Common):LCD 的 公共信号,决定哪个段被驱动,多 COM 则允许减少 I/O 引脚数量。

由开发板原理图可得,LCD屏幕映射到板上对应的SLCDC外设管脚如下:


比如要在第一位显示0,我们需要点亮的是1A,1B,1C,1D,1E,1F六个位,查表得以下结果:
我们应该将SEG3与COM0,COM2,COM3组合,SEG11与COM0,COM2,COM3组合,即可在第一位显示0。具体代码在后续段落讲解。


二、RASC配置
新建文件的方法不再赘述,我们仍然使用Non-Trust Zone,No RTOS的工程模板。
首先更改时钟树外部晶振频率,设置为8MHz:
Stacks→New Stack→Graphics→Segment LCD(r_slcdc) 添加新配置。

Stack→Properties 设置

切换到Pins栏,找到Peripherals→HMI:SLCDC,进行管脚配置,
Segment LCD Pins 管脚配置如下:

三、代码编写
查看手册:
按以下方式打开RA4L1用户手册:

Modules → Graphics → Segment LCD (r_slcdc)
或者直接搜索slcdc。
FSP库下驱动SLCDC模块用到的函数有:
以下是FSP用户手册查找到的函数声明和注释,可供参考:
1.R_SLCDC_Open():
打开SLCDC驱动的函数,确保 SLCDC 可以开始正常运行。运行后还需要执行 R_SLCDC_Start() ,SLCDC才会开始运行。

2.R_SLCDC_Start():
开始输出LCD信号。

3.R_SLCDC_Stop():
停止输出LCD信号。

4.R_SLCDC_Write():
将显示数据序列写入段数据寄存器,是一个批量写入连续段数据的 API,用于高效更新段式 LCD 的多个连续段显示内容。

- start_segment:指定起始段寄存器的编号,即从哪个段开始写入数据。
- p_data:指向uint8_t类型的数据缓冲区指针,存储了要写入段寄存器的一系列数据,不能为空。
- segment_count:指定要连续写入的段数量,即从start_segment开始,一共写入多少个段的内容。
5.R_SLCDC_Modify():
基于掩码和所需数据修改单个段寄存器。

- data:要写入到段寄存器的目标数据(即希望该段寄存器最终呈现的值)。
- data_mask:数据掩码,用于控制 “哪些位需要被修改”。掩码的作用是:只有当 data_mask中某一位为1时,data中对应位的值才会写入寄存器;若data_mask中某一位为 0,则寄存器中该位的值保持不变。
6.R_SLCDC_Close():
关闭SLCDC驱动程序。

(代码示例1)使用R_SLCDC_Write在第一位显示数字1
因为显示1比显示0简单,我们首先编写代码来在第一位显示1:

首先,查表得到要使LCD的第一位显示1,我们需要对SEG11的COM1、COM2两位置为1,即SEG11的值为0b0110,转化为十六进制为0x06:

编写代码如下:
首先使用R_SLCDC_Open打开SLCDC模块:
err = R_SLCDC_Open(&g_slcdc0_ctrl, &g_slcdc0_cfg);
assert(FSP_SUCCESS == err);
然后使用R_SLCDC_Start启用SLCDC的数据传输:
err = R_SLCDC_Start(&g_slcdc0_ctrl);
assert(FSP_SUCCESS == err);
再然后使用R_SLCDC_Write对SEG11的值进行写入,函数定义如下:
fsp_err_t R_SLCDC_Write(slcdc_ctrl_t * const p_ctrl,
uint8_t const start_segment, //开始段编号,比如SEG11则填写11
uint8_t const * p_data, //写入数据的指针
uint8_t const segment_count);
uint8_t segment_data_num1[]={0x06};
R_SLCDC_Write(&g_slcdc0_ctrl, 11, segment_data_num1, sizeof(segment_data_num1));
注意我们要操作的是SEG11,所以R_SLCDC_Write的第二个参数 start_segment我们填入11;第三个参数是一个uint8_t*类型的指针,所以我们必须填入的是地址或者数组名称,故初始化了一个数组用于存储显示num1的值。
在hal_entry中写入上述代码,完整代码如下:
void hal_entry(void)
{
/* TODO: add your own code here */
fsp_err_t err;
err = R_SLCDC_Open(&g_slcdc0_ctrl, &g_slcdc0_cfg);
assert(FSP_SUCCESS == err);
R_BSP_SoftwareDelay(5,BSP_DELAY_UNITS_MILLISECONDS);
err = R_SLCDC_Start(&g_slcdc0_ctrl);
assert(FSP_SUCCESS == err);
uint8_t segment_data_num1[]={0x06};
R_SLCDC_Write(&g_slcdc0_ctrl, 11, segment_data_num1, sizeof(segment_data_num1));
R_BSP_SoftwareDelay(1000,BSP_DELAY_UNITS_MILLISECONDS);
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
(代码示例2)使用R_SLCDC_Write在第二位显示数字2


查表得,要在第二位显示数字2,应该把SEG15与COM1,COM2,COM3组合,SEG16与COM0,COM1组合,因此定义数组:
uint8_t segment_data_num2[]={0x0E, 0x03};
并在第15位起调用 R_SLCDC_Write写入此数组,来显示这两位数,即
R_SLCDC_Write(&g_slcdc0_ctrl, 15, segment_data_num2, sizeof(segment_data_num2));
完整的 hal_entry函数如下:
void hal_entry(void)
{
/* TODO: add your own code here */
fsp_err_t err;
err = R_SLCDC_Open(&g_slcdc0_ctrl, &g_slcdc0_cfg);
assert(FSP_SUCCESS == err);
R_BSP_SoftwareDelay(5,BSP_DELAY_UNITS_MILLISECONDS);
err = R_SLCDC_Start(&g_slcdc0_ctrl);
assert(FSP_SUCCESS == err);
uint8_t segment_data_num1[]={0x06};
uint8_t segment_data_num2[]={0x0E, 0x03};
R_SLCDC_Write(&g_slcdc0_ctrl, 11, segment_data_num1, 1);
R_SLCDC_Write(&g_slcdc0_ctrl, 15, segment_data_num2, sizeof(segment_data_num2));
R_BSP_SoftwareDelay(1000,BSP_DELAY_UNITS_MILLISECONDS);
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
(代码示例3)使用R_SLCDC_Modify在第一位交替显示数字1和0
R_SLCDC_Modify函数定义如下:
fsp_err_t R_SLCDC_Modify(slcdc_ctrl_t * const p_ctrl,
uint8_t const segment_number, //单个段编号,比如SEG11则填写11
uint8_t const data, //写入的数据
uint8_t const data_mask); //掩码,比如0xFF则完全覆盖寄存器的数据,0x00则保留数据,叠加本次数据

我们已经知道要在第一位显示数字0,必须对SEG3和SEG11两个段的寄存器分别写入数据,如果使用R_SLCDC_Write,预先定义的数组需要从第3位开始,写到第11位,即
uint8_t segment_data_num0[]={0x0D,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07};
非常冗长,所以我们可以使用R_SLCDC_Modify函数直接操作单个位,跳过中间无启用的寄存器。
在示例1代码的基础上,我们交替对SEG3和SEG11寄存器进行操作,实现在0和1之间切换的效果:
uint8_t segment_data_num1[]={0x06};
while(1){
//第一位显示1
R_SLCDC_Modify(&g_slcdc0_ctrl,3,0x00,0xFF);
R_SLCDC_Write(&g_slcdc0_ctrl, 11, segment_data_num1, 1);
R_BSP_SoftwareDelay(1000,BSP_DELAY_UNITS_MILLISECONDS);
//第二位显示0
R_SLCDC_Modify(&g_slcdc0_ctrl,3,0x0D,0xFF);
R_SLCDC_Modify(&g_slcdc0_ctrl,11,0x07,0xFF);
R_BSP_SoftwareDelay(1000,BSP_DELAY_UNITS_MILLISECONDS);
}

完整hal_entry实现如下:
void hal_entry(void)
{
/* TODO: add your own code here */
fsp_err_t err;
err = R_SLCDC_Open(&g_slcdc0_ctrl, &g_slcdc0_cfg);
assert(FSP_SUCCESS == err);
R_BSP_SoftwareDelay(5,BSP_DELAY_UNITS_MILLISECONDS);
err = R_SLCDC_Start(&g_slcdc0_ctrl);
assert(FSP_SUCCESS == err);
uint8_t segment_data_num1[]={0x06};
while(1){
R_SLCDC_Modify(&g_slcdc0_ctrl,3,0x00,0xFF);
R_SLCDC_Write(&g_slcdc0_ctrl, 11, segment_data_num1, 1);
R_BSP_SoftwareDelay(1000,BSP_DELAY_UNITS_MILLISECONDS);
R_SLCDC_Modify(&g_slcdc0_ctrl,3,0x0D,0xFF);
R_SLCDC_Modify(&g_slcdc0_ctrl,11,0x07,0xFF);
R_BSP_SoftwareDelay(1000,BSP_DELAY_UNITS_MILLISECONDS);
}
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
(代码示例4)段码LCD显示串口接收的数据
1.查表函数的实现

我们可以发现此LCD屏幕每一位均对应两个SEG管脚,并且每个段相对于COMn管脚的映射是一定的(比如A段一定对应COM0,并且出现在第二列),因此我们可以先用二维数组表示0到9对应的COM的值,分成两列,每一列对应其中一个SEG管脚,每个值是COM3-0组成的四位二进制数:
uint8_t Seg_num[10][2]={
{0x0D,0x07},//0
{0x00,0x06},//1
{0x0E,0x03},//2
{0x0A,0x07},//3
{0x03,0x06},//4
{0x0B,0x05},//5
{0x0F,0x05},//6
{0x00,0x07},//7
{0x0F,0x07},//8
{0x0B,0x07},//9
};
封装为查询函数,可以方便直接获取某个数字的值,函数如下:
uint8_t* get_seg_num(uint8_t num) {
if (num < 1 || num > 9) {
return NULL; // 无效输入返回空指针
}
return Seg_num[num]; // 返回Seg_num[num]的首地址
}
同样的,我们可以用二维数组表示六个位,分别对应的SEGn引脚的标号值:
uint8_t Seg_pos[6][2]={
{03,11},//1p
{15,16},//2p
{22,23},//3p
{24,29},//4p
{30,39},//5p
{40,41},//6p
};
查询函数如下,因为我希望数码管位数的编号从1开始,而数组的编号是从0开始,所以最后的返回值进行了减1操作:
uint8_t* get_seg_pos(uint8_t pos){
if (pos < 1 || pos > 6) {
return NULL; // 无效输入返回空指针
}
return Seg_pos[pos-1]; // 返回Seg_num[num]的首地址
}
要查询相关数据用于显示,可以直接调用上面两个函数;于是我们可以编写一个程序,从左到右依次实现从0到9的显示,并且当刷新到显示屏最后一位(第六位)时,重新从第一位开始显示:

int i = 0;
int j = 1;
while(1){
/*每一次循环,显示的数字+1,显示的位数+1*/
for(i=0;i<10;i++){
R_SLCDC_Modify(&g_slcdc0_ctrl,get_seg_pos(j)[0],get_seg_num(i)[0],0xFF);
R_SLCDC_Modify(&g_slcdc0_ctrl,get_seg_pos(j)[1],get_seg_num(i)[1],0xFF);
R_BSP_SoftwareDelay(1000,BSP_DELAY_UNITS_MILLISECONDS);
j=(j%6)+1; //光标,每次显示的位置都加1,并且大于6时重新设置为1
}
i=0;
}
2.串口接收及显示
我们现在可以灵活控制在段码LCD的任意位上显示任意不同数字,接下来的代码则是通过串口输入任意数字,并在LCD上显示出来。
①串口配置
首先在Stacks→New stack→Connectivity→UART(r_sci_uart)加载UART模块:

串口配置如下:

设置BSP → Property → RA Common → Heap size为0x2000,保证串口缓冲区大小:

②串口接收实现
- 宏定义和全局变量声明
#define TRANSFER_LENGTH 6 //接受6个字符
uint8_t i;
uint8_t j=1;
uint8_t g_out_of_band_received[TRANSFER_LENGTH];
volatile bool g_receive_complete = false;
uint32_t g_out_of_band_index = 0;
uint8_t num_array[6] = {0};
TRANSFER_LENGTH: 定义接收数据长度为6个字符
g_out_of_band_received: 存储原始接收的字符数据
num_array: 存储转换后的数字值
g_receive_complete: 标记是否完成接收(volatile确保多线程可见性)
g_out_of_band_index: 记录当前接收的字符位置
2. UART回调函数实现
以下函数必须是UART的数据位是8bits才能直接使用,如果设置的是9bits的话,需要用两个字节进行处理:
void USER_UART_Callback(uart_callback_args_t * p_args)
{
switch (p_args->event)
{
case UART_EVENT_RX_CHAR: //接收到一个字节则开始以下工作
if (sizeof(g_out_of_band_received) > g_out_of_band_index)
{
uint8_t received_char = (uint8_t)p_args->data;
// 只接收数字字符,过滤非数字字符
if (received_char >= '0' && received_char <= '9')
{
g_out_of_band_received[g_out_of_band_index++] = received_char;
}
// 收到足够字符(6个)后设置完成标志
if (g_out_of_band_index >= TRANSFER_LENGTH)
{
g_receive_complete = 1;
}
}
break;
default:
break;
}
}
③接收数据到显示
- 硬件初始化:打开SLCDC控制器→打开UART9→启动SLCDC显示
void hal_entry(void)
{
fsp_err_t err;
// SLCDC液晶显示控制器初始化
err = R_SLCDC_Open(&g_slcdc0_ctrl, &g_slcdc0_cfg);
assert(FSP_SUCCESS == err);
R_BSP_SoftwareDelay(5,BSP_DELAY_UNITS_MILLISECONDS);
// UART串口初始化
err = R_SCI_UART_Open(&g_uart9_ctrl, &g_uart9_cfg);
assert(FSP_SUCCESS == err);
// 启动SLCDC显示
err = R_SLCDC_Start(&g_slcdc0_ctrl);
assert(FSP_SUCCESS == err);
显示初始化:使用get_seg_pos(j)获取第j个显示位置的段码地址,使用get_seg_num(num_array[j-1])获取对应数字的段码模式,初始显示全0(因为num_array初始化为全0)
// 初始显示设置:显示num_array的初始值,最开始num_array全0
for(j=1;j<=6;j++){
R_SLCDC_Modify(&g_slcdc0_ctrl,get_seg_pos(j)[0],get_seg_num(num_array[j-1])[0],0xFF);
R_SLCDC_Modify(&g_slcdc0_ctrl,get_seg_pos(j)[1],get_seg_num(num_array[j-1])[1],0xFF);
}
- 主循环数据处理
检查是否完成6个字符的接收→
if (g_receive_complete){
将ASCII字符('0'-'9')转换为数值(0-9)→
( 串口接受到的数据是字符串格式,需要转换为数字作为数组下标,可以用传输的数据减去字符'0',比如'1'-'0'得到的就是1)
for (i = 0; i < TRANSFER_LENGTH; i++)
{
num_array[i] = g_out_of_band_received[i] - '0';
}
将转换后的数字显示到对应的6个LCD位置→
for(j=1;j<=TRANSFER_LENGTH;j++){
R_SLCDC_Modify(&g_slcdc0_ctrl,get_seg_pos(j)[0],get_seg_num(num_array[j-1])[0],0xFF); R_SLCDC_Modify(&g_slcdc0_ctrl,get_seg_pos(j)[1],get_seg_num(num_array[j-1])[1],0xFF);
}
清除完成标志和索引,准备下次接收。
g_receive_complete = false;
g_out_of_band_index = 0;
完整的while循环如下:
while(1){
if (g_receive_complete){
// 字符到数字转换
for (i = 0; i < TRANSFER_LENGTH; i++)
{
num_array[i] = g_out_of_band_received[i] - '0';
}
// 更新LCD显示
for(j=1;j<=TRANSFER_LENGTH;j++){
R_SLCDC_Modify(&g_slcdc0_ctrl,get_seg_pos(j)[0],get_seg_num(num_array[j-1])[0],0xFF);
R_SLCDC_Modify(&g_slcdc0_ctrl,get_seg_pos(j)[1],get_seg_num(num_array[j-1])[1],0xFF);
}
// 重置接收状态
g_receive_complete = false;
g_out_of_band_index = 0;
}
R_BSP_SoftwareDelay(10, BSP_DELAY_UNITS_MILLISECONDS);
}
代码放在附件中,另附一份LCD操作手册。
效果展示(拿相机拍手有点抖):
当发送的数据累计达到6位时显示在LCD上。
测评总结
RA4L1的SLCDC模块在低功耗显示应用中表现出色。其硬件自动刷新机制显著降低CPU占用率,只需要对寄存器进行操作,无需软件干预即可维持显示,极大提升了系统能效。驱动能力强劲,可直接控制多段LCD,简化外围电路设计。配合RA4L1的低功耗特性,特别适合电池供电的仪表、家电等长期运行设备。并且SLCDC显示效果稳定无闪烁,刷新效率远超软件模拟方案,是低功耗人机交互应用的理想选择。

全部评论