CH585 上的 LED 点阵屏接口学习使用 ...... 矜辰所致
前言
在 CH585 芯片手册最后部分,介绍了一个名为 LED屏控制器的设备,初看的时候,没太明白,官方也没有太多的说明。
最近正好想用 CH585 控制 WS2812 灯带,与官方 FAE 沟通得知可以使用 LED屏控制 接口,于是回头去研究了一番,嘿嘿,别看其貌不扬,发现可以用在很多地方。
所以本文我们主要就是来聊一下这个 "LED 屏控制器" ,看看如何使用以及它可以适用的场合。
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
- 前言
- [一、 基础说明](#一、 基础说明)
-
- [1.1 使用流程](#1.1 使用流程)
- [1.2 输出对应关系](#1.2 输出对应关系)
-
- [1.2.1 输出位序](#1.2.1 输出位序)
- [1.2.2 输出极性](#1.2.2 输出极性)
- [1.2.3 数据存储模式对输出的影响](#1.2.3 数据存储模式对输出的影响)
- [1.3 多通道输出](#1.3 多通道输出)
- [1.4 时钟](#1.4 时钟)
- [1.5 发送长度](#1.5 发送长度)
- [二、 应用说明](#二、 应用说明)
-
- [2.1 应用核心和场景](#2.1 应用核心和场景)
- [2.2 部分应用示例](#2.2 部分应用示例)
-
- [2.2.1 WS2812 控制](#2.2.1 WS2812 控制)
- [2.2.2 互补 PWM](#2.2.2 互补 PWM)
- [2.2.3 LED点阵屏幕驱动](#2.2.3 LED点阵屏幕驱动)
- [2.2.4 I2S 音频驱动](#2.2.4 I2S 音频驱动)
- [2.2.5 关于模拟 SPI](#2.2.5 关于模拟 SPI)
- 结语
总结写在前面:
CH585 的 LED 屏控制器 就是一个可配置 周期、极性、位序、通道(1~8路)的串行输出总线 ,支持 DMA 传输。
一、 基础说明
官方文档的介绍比较简单:

示例名为 LED :

看示例名字,估计会有不少人联想到点亮 LED 灯的吧 , 官方是没有单独的 GPIO 实验的,GPIO 中断测试大家可参考官方 PM 休眠实验,关于 GPIO 使用也可以参考博主另外一篇文章:
如果单看介绍,估计很多小伙伴觉得没用屏幕就不会再去了解了,博主最初也这样 = =! 前面总结写了,他是一个可配置的串行输出,我们即便不驱动屏幕,也可以在很多地方使用起来。
1.1 使用流程
使用流程参考官方示例即可,整个流程其实很简单,概括来说:
-
设置对应的 GPIO 为输出模式:
通道和 GPIO 的对应关系是固定的,示例一目了然,芯片手册也有标注;
PA4 引脚为时钟输出引脚;
-
LED 驱动器初始化,配置时钟和通道:
使用函数
ch58x_led_controller_init配置分频和模式,第一个参数选择通道数量,第二个参数选择分频系数,通过主频时钟分频(细节会在下文分析)。 -
DMA 配置:
使用函数
TMR_DMACfg配置 DMA,参数依次为:是否打开 DMA 功能,DMA 起始地址,DMA 发送长度,DMA传输模式。 -
输出使能:
使用宏定义
LED_ENABLE();进行输出使能,开始传输。 -
根据需求开启中断:
如果使用 DMA 循环传输,可以不用开启中断,也能够保证持续的输出(每次传输的都是固定的值),如果单次传输,需要开启中断,在中断中调用
ch58x_led_controller_send函数实现连续发送(可以改变传输的值)。
这里还是上一下示例代码,以便于后面讲解时候大家参考,细节我们下文慢慢分析:
c
__attribute__((__aligned__(4))) uint32_t tx_data[8] = {0x01020408,0x10204080,0x03,0x04,0x05,0x06,0x07,0x08};
#define LSB_HSB 0 // LED串行数据位序, 1:高位在前; 0:低位在前
#define POLAR 0 // LED数据输出极性, 0:直通,数据0输出0,数据1输出1; 1为反相
int main()
{
HSECFG_Capacitance(HSECap_18p);
SetSysClock(SYSCLK_FREQ);
/* 配置串口调试 */
DebugInit();
PRINT( "Start @ChipID=%02X\n", R8_CHIP_ID );
//led clk
GPIOA_ModeCfg( GPIO_Pin_4, GPIO_ModeOut_PP_5mA );
//led data
//LED 0
GPIOA_ModeCfg( GPIO_Pin_0, GPIO_ModeOut_PP_5mA );
//LED 1
GPIOA_ModeCfg( GPIO_Pin_1, GPIO_ModeOut_PP_5mA );
//LED 2
GPIOA_ModeCfg( GPIO_Pin_2, GPIO_ModeOut_PP_5mA );
//LED 3
GPIOA_ModeCfg( GPIO_Pin_3, GPIO_ModeOut_PP_5mA );
//lED 4
GPIOA_ModeCfg( GPIO_Pin_5 , GPIO_ModeOut_PP_5mA );
//lED 5
GPIOA_ModeCfg( GPIO_Pin_6 , GPIO_ModeOut_PP_5mA );
//lED 6
GPIOA_ModeCfg( GPIO_Pin_7 , GPIO_ModeOut_PP_5mA );
//lED 7
GPIOA_ModeCfg( GPIO_Pin_8 , GPIO_ModeOut_PP_5mA );
//配置分频和模式选择
ch58x_led_controller_init(CH58X_LED_OUT_MODE_SINGLE, 128);
//开始发送,后面再发送就在中断里面发送了
TMR_DMACfg(ENABLE, (uint16_t)(uint32_t)&tx_data, 2, Mode_Single);
#if LSB_HSB //LSB HSB
R8_LED_CTRL_MOD ^= RB_LED_BIT_ORDER;
#endif
#if POLAR //极性
R8_LED_CTRL_MOD ^= RB_LED_OUT_POLAR;
#endif
LED_ENABLE();
PFIC_EnableIRQ(LED_IRQn);
while(1);
}
__INTERRUPT
__HIGH_CODE
void LED_IRQHandler(void)
{
//清空中断标志
if(LED_GetITFlag(RB_LED_IF_DMA_END)) // 获取中断标志
{
LED_ClearITFlag(RB_LED_IF_DMA_END); // 清除中断标志
ch58x_led_controller_send(tx_data, 2);
}
}
1.2 输出对应关系
在实际使用中,我们需要关心的首要问题就是如何输出,示例中定义了一个uint32_t 的数组,配置 DMA 的起始地址为数组的地址,数组的值就是输出值,那么它是如何体现的呢?
为了更加方便的说明,我们修改一下数组的值,然后还要先说明两个宏定义:
第一个 LSB_HSB 宏定义(关联RB_LED_BIT_ORDER 寄存器)
还一个 POLAR 宏定义(关联RB_LED_OUT_POLAR 寄存器)

1.2.1 输出位序
首先我们只修改数组的值,其他地方均未修改(本小节测试都是单通道 CH58X_LED_OUT_MODE_SINGLE):
c
uint32_t tx_data[2] __attribute__((aligned(4))) = {
0xAAAAAAAA, //
0x01020408 //
};
// LED串行数据位序, 1:高位在前; 0:低位在前
#define LSB_HSB 0
#define POLAR 0
ch58x_led_controller_init(CH58X_LED_OUT_MODE_SINGLE, 128);
//开始发送,后面再发送就在中断里面发送了
TMR_DMACfg(ENABLE, (uint16_t)(uint32_t)&tx_data, 2, Mode_Single);
输出如下(因为是循环输出长度2 ,会先输出 0XAAAAAAAA,下图做图的时候忘了顺序前后了):

上面数据在两个数据交互处稍微不那么直观,为了更加直观的说明这个问题,我们再把数据改一下,然后再看一下:
c
uint32_t tx_data[2] __attribute__((aligned(4))) = {
0xFFFFFFFF, //
0x01020408 //
};
// LED串行数据位序, 1:高位在前; 0:低位在前
#define LSB_HSB 0

上面的示例,如果我们把 LSB_HSB 设置位 1,它会变成如下:

说明:LSB_HSB 设置为1,依然是数组前一个 uint32_t 先输出,然后根据每个字节进行高位在前反转。

1.2.2 输出极性
最后还有一个 POLAR 宏定义(关联RB_LED_OUT_POLAR 寄存器),反相输出,这个比较好理解,我们测试看一下:

1.2.3 数据存储模式对输出的影响
对了,上面的测试,我们定义0x01020408 ,但是输出却是反过来的,这是于芯片存储是小端模式有关:

我们存放 0x01020408 ,起始地址第一个字节实际上是 0x08 所以会按照 0x08先输出。
比如我们修改一下,按照单字节定义数组,它就能够按照顺序输出:
c
uint8_t tx_data[] __attribute__((aligned(4))) = {
0xFF,0xFF,0xFF,0xFF, //
0x01,0x02,0x04,0x08 //
};

此时函数中的长度参数,依旧是 2 。
通过上面测试,我们可以得到如下结论:
TMR_DMACfg函数中的第三个参数长度是以 32 位 4字节为单位的,同理,中断中调用的ch58x_led_controller_send函数的第二个参数长度也是这样。
函数的参数长度是以 32 位 4字节为1 个长度标准。
所以原始示例中,只会用到数组前面两个数据。- 数据输出是以 1 个字节为单位进行输出,会以起始地址开始按照每个字节的数据进行输出。
LSB_HSB这个串行数据位序,高位在前还是低位在前是针对每个字节来说的。
另外,示例中使用了中断方式,在TMR_DMACfg 函数中模式配置为:Mode_Single 需要保证持续发送,需要在 DMA发送完成中断中,再次调用ch58x_led_controller_send(tx_data, 2);进行发送。我们如果配置 DMA 模式为 Mode_LOOP,可以关闭中断,就能持续发送,如下:
c
TMR_DMACfg(ENABLE, (uint16_t)(uint32_t)&tx_data, 2, Mode_LOOP); //无需开启中断即可连续发送
1.3 多通道输出
上面测试的都单通道,它支持1/2/4/8路数据输出,但是要注意,它也只能支持1/2/4/8, 比如 3 是不行的,而且对应通道是固定的(1路 固定 LED0 ,2路固定 LED0 和 LED1... GPIO 引脚也是固定的 ):

我们还是使用上面的数据,测试一下双通道输出的效果:
c
uint32_t tx_data[2] __attribute__((aligned(4))) = {
0xFFFFFFFF, //
0x01020408 //
};
#define LSB_HSB 0 // LED串行数据位序, 1:高位在前; 0:低位在前
#define POLAR 0 // LED数据输出极性, 0:直通,数据0输出0,数据1输出1; 1为反相
ch58x_led_controller_init(CH58X_LED_OUT_MODE_DOUBLE, 128);
效果如下:

通道0 →0xFF 0xFF 0x08 0x02
通道1 →0xFF 0xFF 0x04 0x01
它会按照需要输出的通道,按字节顺序依次去分给所有的通道,所以我们可以推算出来,如果设置位 4 通道,那么4通道在每 8 个时钟周期的输出应该是:
通道0 → 0xFF 0x08
通道1 → 0xFF 0x04
通道2 → 0xFF 0x02
通道3 → 0xFF 0x01
测试一下ch58x_led_controller_init(CH58X_LED_OUT_MODE_FOUR, 128);确实如此,如下图:

以此类推,对于 { 0xFFFFFFFF, 0x01020408 } ,8通道输出如下:
通道0 → 0xFF
通道1 → 0xFF
通道2 → 0xFF
通道3 → 0xFF
通道4 → 0x08
通道5 → 0x04
通道6 → 0x02
通道7 → 0x01

所以,如果想要 8 通道都有输出,那么至少需要 8 个字节的数据,始终记住,数据是以字节为单位进行输出即可。
1.4 时钟
LED 屏控制器的时钟来源于系统主频,通过 ch58x_led_controller_init 设置分频系数,示例中是 128 分频:
频率:
62.4 MHz ÷ 128 = 487.5 kHz
周期:
1 ÷ 487.5 kHz ≈ 2.05 µs

说明: 这是是串行时钟线的时钟,高低电平时间不对称是经常的事,时钟信号的占空比不是一定要 50%,只要满足协议规定的最小高电平时长和最小低电平时长,满足"采样窗口" 要求即可。
最大时钟:
主频 2 分频输出如下图(主频 62.4M Hz 情况):

最小时钟:
最大分频系数为 0xFF, 所以 :
最小时钟频率 = 主频/255

1.5 发送长度
我们上面几个函数中有个参数为发送长度,他对应的就是寄存器R16_LED_DMA_LEN :

它的最大值为:4095
我们上面说过,长度是以 32 位为单位,就是 4个字节,所以可以知道它一次发送最大的长度为:4095 × 4 字节 = 16 380 字节 。
二、 应用说明
2.1 应用核心和场景
经过上面的基础学习,就可以得出我们文章前面的总结:CH585 的 LED 屏控制器 就是一个支持 DMA 传输的可配置 周期、极性、位序、通道(1~8路)的串行输出总线 。
理论上,只要是串行 输出 (注意是输出)总线,在它能支持频率范围,都能用它来模拟实现:
- 只能当"主机",不能当从机。
- 时钟频率范围 : 芯片主频 / 255 ~ 芯片主频 / 2
使用的基本流程就是,首先是确定时钟周期,然后根据数据设定数组。
下面表格列举了一些常用可支持应用场景:
| 应用 | 说明 | 思路 |
|---|---|---|
| 驱动点阵屏 | 屏幕驱动 | 配合 74HC595, 可同时驱动多个 595,扩展出 8×N 路输出(如 2 片 595 扩展 16 路); |
| WS2812 灯带 | 灯带驱动 | ① 1 颗灯 = 24 位码 ;② PA0→DATA,单通道;③ 分频 ;④ 根据需求设定数据 |
| 互补型 PWM | PWM输出 | 双通道实现互补型 PWM 波 |
| I²S 音频 | 音频驱动 | PA0→DATA,PA1→LRCK,双通道 PA4 : 时钟信号; |
| DShot ESC | 无人机协议 | DShot 是一种固定波特率、固定帧结构、单向、无载波的数字串行协议 ,和 CH585 LED 屏控制器完全匹配 |
2.2 部分应用示例
一些常见应用示例,其实知道上面应用核心,剩下的都是数据的处理部分,本文先展示一些常见的示例。
2.2.1 WS2812 控制
灯带是一个比较典型的示例,我会从本示例入手详细介绍一下如何使用 CH585 的 LED 屏控制器。
对于 WS2812 和 SK6812 此类 灯带,控制方式是一致的,具体的例子网上很多,而且采购的灯带也会有对应的控制手册,博主在以前也写过灯带的控制说明,我们需要参考的细节如下:

下面的截图说明是以前文章里面的,因为于上面的截图不是来自一个型号的手册,所以时间有差别(下面我以前是使用 SK6812 来讲解的,WS2812 的顺序大多是 GRB):



灯带的控制方式知道了,我们来考虑我们 LED 屏控制器设置。
首先我们先考虑的是,我们如何输出 0/1 码,因为他们是由高低电平组成,而且有时间要求,所以我们肯定是需要 GPIO 输出好几位数据组成一个码。
思路如下:
比如我们可以用 4 位, 也可以用 8 位(1个字节)。
假设我们用 4 位去拼凑,
我们 GPIO 输出 0x80 = 0b1000 000,
前面 4 位,1个时钟周期高电平,3 个时钟周期低电平
如果保证 4个时钟周期 >=1.25us(不同型号有细微区别)
而且高电平时间满足:220ns~380ns
低电平时间满足:580ns~1µs
那就是一个 0 码
就等于我们 GPIO 输出 0b1000 ,就是输出一个0 码,1个字节可以输出2个 码
这样就可以实现组合了。
根据上面的思路,我们可以计算,或者直接通过测试慢慢调整,最后采用如下配置,可以实现满足上面的要求:
c
SetSysClock(CLK_SOURCE_HSE_PLL_78MHz);
ch58x_led_controller_init(CH58X_LED_OUT_MODE_SINGLE, 25);

确定好时钟,接下来就是数据组合了,根据上面设置:
输出一个 0 码:是发送 0b1000 (0x8);
输出一个 1 码: 是发送 0b1100(0xC).
那我们实现亮红灯:
对应 0/1 码
1111 1111 0000 0000 0000 0000 (SK6812)
0000 0000 1111 1111 0000 0000 (WS2812)
具体的还请参考使用灯珠的手册确定!!!
我们一个字节两个码,我们定义数组如下():
c
uint8_t tx_data[] __attribute__((aligned(4))) = {
0x88,0x88,0x88,0x88, // G 0000 0000
0xCC,0xCC,0xCC,0xCC, // R 1111 1111
0x88,0x88,0x88,0x88 // B 0000 0000
};
如果两个灯,那就是对应的发送和长度改为 6 即可:
c
uint8_t tx_data[] __attribute__((aligned(4))) = {
0x88,0x88,0x88,0x88, // G 0000 0000
0xCC,0xCC,0xCC,0xCC, // R 1111 1111
0x88,0x88,0x88,0x88, // B 0000 0000
0x88,0x88,0x88,0x88, // G 0000 0000
0x88,0x88,0x88,0x88,
0xCC,0xCC,0xCC,0xCC
};
GPIOA_ModeCfg( GPIO_Pin_0, GPIO_ModeOut_PP_5mA );
ch58x_led_controller_init(CH58X_LED_OUT_MODE_SINGLE, 25);
TMR_DMACfg(ENABLE, (uint16_t)(uint32_t)&tx_data, 6, Mode_Single);
博主测试自己以前的板子和灯带效果如下:

上面演示了单色点亮,那么对于渐变,需要自己去对应修改数据,然后重发方式,可以根据示例里面再中断中发送,但是要注意灯带需要的 RESET 信号延时,也可以直接在 While 循环中发送:
c
//下面是伪代码,示例代码,需要自己实现
while(1){
// i 是渐变数组,在 i 组数据里面循环渐变
if(i < 5) i++;
else i = 0;
ch58x_led_controller_send(&change_data[6*i],6);
DelayUs(100);// LED 的 RESET 信号
}
当然,上面使用 4 个时钟周期表示一个 0/1 码,当然也可以使用 8 个时钟周期表示一个 0/1 码,这个大家可以自行实现。
2.2.2 互补 PWM
PWM 输出理解起来应该比较简单,对于 CH585 的 PWM输出,博主也写过一篇文章:
在 CH585 上,我们使用 LED 控制器也可以实现 PWM ,需要注意的是它只能实现 1/32 倍数的占空比。重点在于,我们利用 CH585 的 LED 屏驱动,可以输出互补的 PWM 波。
首先,我们实现 50% 占空比的 PWM 波(周期根据自己需要的时钟分频):
c
uint8_t tx_data[] __attribute__((aligned(4))) = {
0xFF,0xFF,0x0,0x0, //
};
TMR_DMACfg(ENABLE, (uint16_t)(uint32_t)&tx_data, 1, Mode_LOOP);

原理很简单一次发送 32 位数据:
0b 10000000 00000000 00000000 00000000 : 1/32 占空比
0b 11111111 111111111 11111111 11110000 : 28/32 占空比
互补的 PWM 波形,示例如下:
c
uint8_t tx_data[] __attribute__((aligned(4))) = {
0xFF,0x00,0xF0,0x0F, //
};
ch58x_led_controller_init(CH58X_LED_OUT_MODE_DOUBLE, 25);

2.2.3 LED点阵屏幕驱动
本来接口就是作为屏幕控制接口来使用,我们配和 74HC595 驱屏连接如::
| 74HC595 引脚 | CH585 引脚 | 说明 |
|---|---|---|
| SER (DS) | PA0 | 串行数据(单通道时,多一个74HC595 就多使用一个通道) |
| SRCLK (SH_CP) | PA4 | 移位时钟(由 LED 控制器自动生成) |
| RCLK (ST_CP) | 任意 GPIO | 需手动控制(LATCH) |
| OE | GND | 输出使能(常低) |
| MR | VCC | 复位(常高) |
但是博主目前手头还没有屏幕,这个后期需要使用的时候再来补充把。
待更新!
2.2.4 I2S 音频驱动
I2S 的 3个主要信号线:
- SCK/BCLK(串行时钟/位时钟)控制每个位的传输时序,我们直接使用 PA4 模拟。
频率 = 2 × 采样率 × 位深度 × 声道数
例:44.1kHz × 16位 × 2声道 × 2 = 2.8224MHz
每个时钟周期传输1位数据
- WS/LRCLK(字选择/左右声道时钟),使用通道 2 :PA1作为帧选信号。
区分左右声道:
WS=0 → 左声道
WS=1 → 右声道
在每个声道数据传输开始时改变
- SD/SDOUT/SDIN(串行数据线)实际音频数据,我们使用 PA0 作为输出。
注意,数据方式通常MSB(最高有效位)在前
注意在标准模式下面:
时序规则明确:
声道区分:LRCK低电平=左声道,高电平=右声道;
数据采样/发送沿:数据在SCK的下降沿发送,上升沿采样(确保数据稳定后再读取);
时序延迟:有效数据相对于LRCK的跳变沿延迟1个SCK时钟周期;
对齐方式:数据的MSB与LRCK跳变沿延迟1个SCK边沿对齐。
所以我们在定义数据的时候,需要实际音频数据(PA0)需要比片选信号(PA1)晚一位开始定义有效数据。
具体后期有机会再来更新。
2.2.5 关于模拟 SPI
起初我以为是可以模拟 SPI 的,但是真正研究了一下才发现,有些问题:
因为我们的 LED 控制器时钟空闲为低电平,在时钟的第二个上升沿采样,而标准 SPI 只有 4 种模式,无法匹配标准 SPI 的是时钟信号:
模式0:CPOL=0, CPHA=0 -> 空闲时SCLK为低电平,数据在第一个边沿(即上升沿)采样。
模式1:CPOL=0, CPHA=1 -> 空闲时SCLK为低电平,数据在第二个边沿(即下降沿)采样。
模式2:CPOL=1, CPHA=0 -> 空闲时SCLK为高电平,数据在第一个边沿(即下降沿)采样。
模式3:CPOL=1, CPHA=1 -> 空闲时SCLK为高电平,数据在第二个边沿(即上升沿)采样。
如下图:

本来还想着确实无法模拟,但是忽然想到,是否可以使用一路数据替代时钟信号,但是想想,这样做还是有些问题,数据就不仅被 " 分割 " 了,而且因为新的时钟是 "翻倍 " 的,导致以前的 2bit 才能表示新的 1bit ,而且还要考虑时钟空闲状态,一系列问题,还是算了。
结语
本文主要了解了一下 CH585 的 LED 屏控制器的基本原理、使用方式,以及一些可行的应用场景,其中有些功能并没有完全的写出示例,博主手头的设备也是有限的嘛,当然在后期遇到的时候会来一一记录。
其实关键的地方还是只需要记住,它是如何输出的。应用的时候只需要线确定时钟周期,然后再根据自己需要的输出定义数组。
好了,本文就到这里。谢谢大家!