CH585 的 LED 屏控制器(串行输出接口)

复制代码
 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 使用也可以参考博主另外一篇文章:

沁恒微 RISC-V 蓝牙 CH5xx GPIO使用说明

如果单看介绍,估计很多小伙伴觉得没用屏幕就不会再去了解了,博主最初也这样 = =! 前面总结写了,他是一个可配置的串行输出,我们即便不驱动屏幕,也可以在很多地方使用起来。

1.1 使用流程

使用流程参考官方示例即可,整个流程其实很简单,概括来说:

  1. 设置对应的 GPIO 为输出模式:

    通道和 GPIO 的对应关系是固定的,示例一目了然,芯片手册也有标注;

    PA4 引脚为时钟输出引脚;

  2. LED 驱动器初始化,配置时钟和通道:

    使用函数 ch58x_led_controller_init 配置分频和模式,第一个参数选择通道数量,第二个参数选择分频系数,通过主频时钟分频(细节会在下文分析)。

  3. DMA 配置:

    使用函数 TMR_DMACfg配置 DMA,参数依次为:是否打开 DMA 功能,DMA 起始地址,DMA 发送长度,DMA传输模式。

  4. 输出使能:

    使用宏定义 LED_ENABLE();进行输出使能,开始传输。

  5. 根据需求开启中断:

    如果使用 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 。

通过上面测试,我们可以得到如下结论:

  1. TMR_DMACfg 函数中的第三个参数长度是以 32 位 4字节为单位的,同理,中断中调用的 ch58x_led_controller_send 函数的第二个参数长度也是这样。
    函数的参数长度是以 32 位 4字节为1 个长度标准。
    所以原始示例中,只会用到数组前面两个数据。
  2. 数据输出是以 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输出,博主也写过一篇文章:

CH58x 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个主要信号线:

  1. SCK/BCLK(串行时钟/位时钟)控制每个位的传输时序,我们直接使用 PA4 模拟。

频率 = 2 × 采样率 × 位深度 × 声道数

例:44.1kHz × 16位 × 2声道 × 2 = 2.8224MHz

每个时钟周期传输1位数据

  1. WS/LRCLK(字选择/左右声道时钟),使用通道 2 :PA1作为帧选信号。

区分左右声道:

WS=0 → 左声道

WS=1 → 右声道

在每个声道数据传输开始时改变

  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 屏控制器的基本原理、使用方式,以及一些可行的应用场景,其中有些功能并没有完全的写出示例,博主手头的设备也是有限的嘛,当然在后期遇到的时候会来一一记录。

其实关键的地方还是只需要记住,它是如何输出的。应用的时候只需要线确定时钟周期,然后再根据自己需要的输出定义数组。

好了,本文就到这里。谢谢大家!

相关推荐
矜辰所致1 个月前
沁恒 RISC-V 蓝牙芯片 Flash 分区管理及操作
risc-v·flash·flash读写·ch585·蓝牙 ble
矜辰所致3 个月前
【导航】沁恒微 RISC-V 蓝牙 入门教程目录 【快速跳转】
沁恒微·蓝牙·risc-v·ble·ch585