☰
STM32F103驱动JXI5020GP恒流LED芯片:SPI与PWM调光实战
2026/10/10 4:40:55 网站建设 项目流程

简介:本资源是一套面向嵌入式初学者与硬件开发者的STM32F103平台LED恒流驱动实战代码,专为适配国产JXI5020GP(兼容TI TLC59281DBQR)16通道恒流LED驱动芯片设计,解决多路LED亮度精准控制、串行数据同步锁存及输出使能管理等典型硬件驱动问题。压缩包仅含2个核心文件(1个C源文件+1个H头文件),总大小2KB,结构精简:C文件实现初始化、数据写入、锁存与消隐控制逻辑,H文件定义关键IO口(PA4/PA5/PA6/PC13)及状态缓存变量,便于快速集成至现有STM32工程。已有883人学习下载,代码注释清晰、接口规范,提供完整IO映射说明与函数功能注解,可直接移植用于LED点阵屏、广告灯板或工业指示灯等需恒流驱动的嵌入式项目,显著降低硬件驱动开发门槛与调试周期。

1. 项目背景与核心需求解析

最近在做一个LED灯板项目,需要驱动几十颗高亮LED。直接拿单片机的IO口去推肯定不行,电流不够,亮度上不去,还容易烧片子。市面上常见的方案是用三极管或者MOS管搭建恒流电路,但每个通道都得配一堆外围元件,PCB画起来麻烦,一致性也难保证。所以,这次把目光投向了专用的LED恒流驱动芯片。

我选中的是JXI5020GP,这颗芯片其实就是TI(德州仪器)TLC59281DBQR的国产兼容型号。它是一颗16通道的恒流LED驱动器,每个通道能提供最高120mA的恒定电流,并且可以通过一个外接电阻来统一设定所有通道的电流值。对于STM32F103这类资源有限的单片机来说,用它来驱动LED阵列简直是绝配——只需要3根线(时钟、数据、锁存)就能控制16路LED,大大节省了IO口,也简化了硬件设计。

这个项目就是围绕STM32F103C8T6(也就是常说的“蓝桥杯”或“最小系统板”上的那颗芯片)来编写驱动JXI5020GP(TLC59281DBQR)的程序。网上能找到的源码要么是针对Arduino的,要么是51单片机的,直接套用到STM32上,特别是用到高级定时器做PWM调光时,总会遇到各种时序和配置上的坑。我把调试通过的完整工程打包成了LED恒流源芯片JXI5020GP(TLC59281DBQR)驱动程序源码.zip,这里就来详细拆解一下里面的门道,特别是如何利用STM32的硬件SPI和定时器实现高效、稳定的驱动。

2. 芯片选型与硬件连接要点

为什么是JXI5020GP/TLC59281DBQR?在做LED点阵、灯带、背光或者任何需要多路恒流LED驱动的场合,我们通常有几个核心诉求:通道数多、控制简单、恒流精度高、支持PWM调光。这颗芯片几乎完美命中。

它采用CMOS工艺,16个通道的电流值由一个外接电阻Rext统一设定,公式是Iout = 31.5 / Rext。比如,你需要每路20mA电流,那么Rext就应该是31.5 / 0.02 ≈ 1575Ω,取个标称值1.6kΩ就行。这种设计比每路单独调电阻省事太多了,而且芯片内部的电流镜像结构保证了各通道间的一致性,实测下来16路之间的电流差异可以控制在3%以内,对于大多数视觉应用来说完全足够。

硬件连接上,重点说三个部分:

  1. 电源与地:芯片的VCC(逻辑电源)和VLED(LED驱动电源)最好分开。VCC接3.3V,与STM32共地。VLED则根据你LED的正向电压来定,比如驱动白光LED可能需要12V。两个电源之间加个磁珠或0Ω电阻隔离一下噪声有好处。
  2. 数据接口:芯片支持标准的串行移位输入,协议很像74HC595。核心三根线:
    • SDI:串行数据输入,接STM32的MOSI引脚(如果使用SPI)。
    • CLK:串行时钟输入,接STM32的SCK引脚。
    • LE:锁存使能(也叫LAT),数据移位完成后,给一个高电平脉冲,将移位寄存器中的数据锁存到输出级。这个引脚可以接STM32的任意GPIO。
  3. 外围电路:Rext电阻要靠近芯片的REXT引脚放置,并且一定要接一个0.1uF的陶瓷电容到地,用于稳定基准电流。每个LED输出通道OUTn到VLED之间,建议串联一个几十欧姆的小电阻(如10Ω),作为额外的限流和阻尼,防止瞬间冲击电流。

注意:芯片的OE(输出使能)引脚是低电平有效。你可以把它直接接地,让输出常开。但更推荐的做法是接一个STM32的PWM引脚,这样可以通过硬件PWM统一控制所有16路LED的亮度,实现全局调光,这个后面会细说。

3. STM32F103的SPI驱动实现详解

驱动这类串行芯片,最优雅的方式就是使用STM32的硬件SPI外设。相比用GPIO模拟时序,硬件SPI不占用CPU资源,速度更快更稳定。STM32F103的SPI配置有几个坑点需要注意。

3.1 SPI外设初始化与模式选择

JXI5020GP的数据传输时序要求是:在CLK上升沿采样SDI数据。这对应SPI的模式0(CPOL=0, CPHA=0)或者模式3(CPOL=1, CPHA=1)。我习惯用模式0,即时钟空闲时为低电平,在第一个边沿(上升沿)采样。

以下是SPI1的初始化代码关键点(以标准库为例):

void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // 2. 配置PA5(SCK), PA7(MOSI)为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 配置PA6(MISO)为输入,虽然我们用不到,但最好初始化 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); // 4. 配置SPI1参数 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主模式 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟空闲低电平 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 第一个边沿采样(上升沿) SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件管理NSS(片选),这里我们不用 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 波特率预分频,APB2时钟36M/8=4.5M SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先行(MSB First) SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); // 5. 使能SPI1 SPI_Cmd(SPI1, ENABLE); }

这里的关键是SPI_FirstBit_MSB。JXI5020GP的数据格式是高位(MSB)先入。第一个移入的数据位,对应芯片内部移位寄存器的第15位(OUT15),最后一个移入的对应第0位(OUT0)。顺序千万不能错。

3.2 数据发送函数与16通道数据组织

发送函数很简单,但要注意连续发送多个字节时的时序。我们需要发送两个字节(16位)的数据来控制16个通道。

void JXI5020_WriteData(uint16_t data) { // 假设LE锁存引脚连接在PA4 #define LE_GPIO_Port GPIOA #define LE_GPIO_Pin GPIO_Pin_4 // 先发送高8位,再发送低8位 SPI_I2S_SendData(SPI1, (uint8_t)(data >> 8)); // 发送高字节 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 等待发送完成 SPI_I2S_SendData(SPI1, (uint8_t)(data & 0xFF)); // 发送低字节 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 所有数据移位完成后,产生一个LE锁存脉冲 GPIO_SetBits(LE_GPIO_Port, LE_GPIO_Pin); // LE拉高 delay_us(1); // 保持高电平至少20ns,这里1us足够 GPIO_ResetBits(LE_GPIO_Port, LE_GPIO_Pin); // LE拉低,完成锁存 }

如何组织你的data变量?一个直观的方法是定义一个16位的变量,每一位对应一个LED通道。1表示该通道LED亮,0表示灭。例如,data = 0xAAAA(二进制1010 1010 1010 1010)会让所有奇数通道(OUT1, OUT3, ... OUT15)点亮。

实操心得:如果你需要频繁更新LED状态,不要在JXI5020_WriteData函数里每次都操作LE引脚。更好的做法是,将LE引脚也配置为SPI的硬件NSS引脚(如果支持),或者使用DMA进行SPI传输,在DMA传输完成中断里再触发LE脉冲,这样可以最大化SPI总线效率,实现无闪烁刷新。

4. 利用PWM实现LED亮度调节与呼吸灯效果

仅仅控制LED亮灭是不够的,我们经常需要调节亮度,甚至做出呼吸灯、流水灯等效果。JXI5020GP本身不支持每个通道独立的PWM,但它有一个OE引脚。当OE为低电平时,输出开启;为高电平时,输出关闭。如果我们给OE引脚输入一个PWM信号,那么所有16个通道的LED就会同步地被这个PWM信号调制,实现全局亮度调节。

4.1 STM32定时器PWM输出配置

STM32F103的通用定时器(TIM2, TIM3, TIM4)和高级定时器(TIM1, TIM8)都支持PWM输出。我们以TIM3的通道2(对应引脚PA7,但PA7我们已用作SPI1_MOSI,所以这里改用TIM2_CH1 PA0来驱动OE引脚)为例。

目标是产生一个频率可调、占空比可变的PWM波。PWM频率的选择有讲究:频率太低(比如100Hz以下),人眼会感觉到闪烁;频率太高,可能会受限于芯片OE引脚的响应速度。一般选择200Hz到1kHz之间比较合适。

void PWM_Init(uint16_t arr, uint16_t psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 2. 配置PA0为复用推挽输出(TIM2_CH1) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 初始化定时器基础参数 // 假设系统时钟72MHz,APB1预分频为2,所以TIM2时钟为72MHz TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值,决定PWM频率 TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频系数 TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); // 4. 初始化PWM输出比较参数 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 使能输出 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性高 TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比为0(LED全灭) TIM_OC1Init(TIM2, &TIM_OCInitStructure); // 初始化通道1 TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); // 使能预装载寄存器 // 5. 使能定时器 TIM_Cmd(TIM2, ENABLE); }

计算PWM频率:PWM_Freq = TIM_CLK / ((arr + 1) * (psc + 1))。例如,TIM_CLK=72MHz, 设置psc=71,arr=999, 则频率为72,000,000 / ((71+1)*(999+1)) = 1000Hz。

4.2 动态改变占空比实现呼吸灯

有了PWM硬件,实现呼吸灯就非常简单了。我们只需要在一个定时器中断或者主循环里,周期性地改变PWM的占空比(即TIM_Pulse的值)即可。

// 在main.c或某个定时器中断服务函数中 void TIM2_IRQHandler(void) { // 假设用TIM2的更新中断来更新占空比 if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { static uint16_t pwmVal = 0; static int8_t dir = 1; // 方向,1为增加,-1为减少 pwmVal += dir * 10; // 每次改变10个单位 if (pwmVal >= 1000) { // 到达最大值(arr=999) dir = -1; pwmVal = 1000; } else if (pwmVal <= 0) { // 到达最小值 dir = 1; pwmVal = 0; } TIM_SetCompare1(TIM2, pwmVal); // 更新占空比 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }

这段代码会让PWM的占空比从0到100%再到0循环变化,连接到OE引脚后,所有LED就会呈现呼吸灯效果。注意,这里OE是低电平有效,所以PWM极性设置为高,意味着占空比越大,OE高电平时间越长,LED熄灭时间越长,视觉上反而越暗。如果你希望占空比越大LED越亮,可以将TIM_OCPolarity设置为TIM_OCPolarity_Low。

4.3 独立通道亮度控制(软件PWM方案)

全局PWM限制了所有通道必须同亮同暗。如果想实现每个LED独立的呼吸灯或亮度控制,就需要用到软件PWM,或者叫时分复用PWM。

原理是:我们设定一个比视觉刷新率快得多的基础定时器中断(比如1ms)。在这个中断里,维护一个全局的亮度计数器(比如0-255级)和每个通道独立的亮度目标值。在每次中断中,将每个通道的亮度目标值与全局计数器比较,如果目标值大于计数器,则点亮该通道;否则熄灭。这样,在一个PWM周期内,每个通道点亮的时长就与其目标值成正比,实现了独立调光。

#define LED_CHANNELS 16 #define PWM_MAX_LEVEL 255 uint8_t ledBrightness[LED_CHANNELS] = {0}; // 存储16个通道的亮度值(0-255) uint8_t pwmCounter = 0; // 全局PWM计数器 uint16_t ledOutputState = 0; // 当前要输出的16位状态 // 在1ms定时器中断中 void TIM4_IRQHandler(void) { // 假设TIM4做1ms中断 if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET) { pwmCounter++; ledOutputState = 0; // 清零状态 // 遍历所有通道,比较亮度值与计数器 for (int i = 0; i < LED_CHANNELS; i++) { if (ledBrightness[i] > pwmCounter) { ledOutputState |= (1 << i); // 该位置1 } } // 将新的状态通过SPI发送给JXI5020GP JXI5020_WriteData(ledOutputState); TIM_ClearITPendingBit(TIM4, TIM_IT_Update); } }

然后,你只需要在程序的其他地方修改ledBrightness数组里的值,就可以独立控制每个LED的亮度了。这种方法的缺点是刷新率受限于中断频率和SPI发送时间。如果通道很多,中断服务函数执行时间过长,可能会影响系统其他任务。对于16通道,1ms中断周期是完全可以接受的。

5. 驱动代码的优化与高级应用

基础的驱动和调光功能实现后,我们可以考虑一些优化和更复杂的应用场景,让代码更健壮、效率更高。

5.1 使用DMA+SPI实现后台无阻塞刷新

在软件PWM方案中,每次定时器中断都要调用JXI5020_WriteData函数,里面包含SPI发送和等待。如果SPI速度不够快,会长时间占用CPU。使用DMA可以将数据搬运到SPI数据寄存器的任务交给硬件,CPU在启动传输后就可以去处理其他事情。

配置SPI的DMA发送大致步骤如下:

  1. 配置SPI为只发送模式(SPI_Direction_1Line_Tx)。
  2. 使能SPI的DMA发送请求(SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE))。
  3. 配置DMA通道(例如SPI1_TX用DMA1_Channel3),设置内存地址(你的数据缓冲区地址)、外设地址(&SPI1->DR)、数据长度、传输方向(内存到外设)。
  4. 在DMA传输完成中断中,触发LE锁存脉冲。

这样,你只需要更新内存中的亮度数据缓冲区,然后启动一次DMA传输,剩下的工作硬件会自动完成,极大地提高了效率,也为实现更复杂的动画效果(如WS2812灯带那种)提供了可能。

5.2 灰度校正与Gamma校正

人眼对光强的感知是非线性的。LED的亮度(电流)与PWM占空比(或亮度值)呈线性关系,但我们感觉到的亮度变化却不是线性的。在低亮度区域,人眼对变化更敏感;在高亮度区域,敏感度降低。直接使用线性PWM值,会导致亮度调节不均匀,低亮度时跳变太快,高亮度时变化不明显。

为了解决这个问题,需要进行Gamma校正。通常的做法是预先计算一个Gamma查找表(LUT),将线性的亮度索引(0-255)映射到非线性的PWM输出值上。

// 生成Gamma=2.2的查找表 const uint8_t gammaLUT[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // ... 中间数值需要根据公式 pow(i/255.0, 2.2) * 255 计算并四舍五入 255 }; // 当设置亮度时,使用查找表 void setLedBrightness(uint8_t ch, uint8_t linearBrightness) { if (ch < LED_CHANNELS) { ledBrightness[ch] = gammaLUT[linearBrightness]; } }

经过Gamma校正后,你传入的linearBrightness值(比如从0到255线性增加),对应的LED视觉亮度变化就会显得平滑且均匀。这是专业灯光控制中必不可少的一步。

5.3 多片JXI5020GP级联驱动更多LED

一颗JXI5020GP只有16个通道。如果需要驱动32、48甚至更多LED怎么办?答案是级联。芯片的SDO引脚就是串行数据输出,可以连接到下一颗芯片的SDI引脚。

级联时,数据发送顺序需要特别注意。假设级联N片,你需要发送 N * 16 位数据。最先发送的数据,会进入最后一片芯片的移位寄存器。例如,级联2片(共32通道),你想让第一片的OUT0亮,第二片的OUT15亮,那么你需要构造一个32位的数据:

  • 位31 ~ 位16: 对应第二片芯片的OUT15 ~ OUT0。
  • 位15 ~ 位0: 对应第一片芯片的OUT15 ~ OUT0。 所以,你需要发送的数据是:(1 << 31) | (1 << 0)(假设其他位为0)。在发送时,先发送这个32位数的高16位,再发送低16位。所有数据依次移位通过所有芯片后,再给一个统一的LE脉冲,所有芯片同时更新输出。

驱动代码需要做相应修改,数据缓冲区变为uint16_t dataBuffer[N](N为芯片数),发送函数改为循环发送N个16位数据。

6. 项目源码结构解析与移植指南

最后,简单介绍一下提供的驱动程序源码.zip里的工程结构,以及如何移植到你的项目中。

工程基于Keil MDK开发,使用STM32标准外设库。主要文件如下:

  • Core/:主循环、系统初始化。
  • Hardware/:硬件驱动层。
    • jxi5020.c/.h:JXI5020GP芯片的驱动核心,包含SPI初始化、数据发送、亮度设置(软件PWM)接口。
    • pwm.c/.h:定时器PWM输出配置,用于OE引脚全局调光。
    • timer.c/.h:基础定时器配置,用于产生1ms中断,实现软件PWM。
  • Application/:应用层示例。
    • led_effects.c/.h:包含几种LED效果示例,如呼吸灯、流水灯、随机闪烁等。

移植步骤:

  1. 硬件对接:确认你的STM32F103开发板上,SPI引脚(PA5, PA7)、LE锁存引脚、OE引脚(如果使用)与代码中的定义一致。如果不一致,修改jxi5020.h和pwm.h中的宏定义。
  2. 时钟配置:工程默认使用外部8MHz晶振,经PLL倍频到72MHz。如果你的板子时钟不同,需要修改system_stm32f10x.c文件或使用STM32CubeMX重新生成时钟配置。
  3. 功能裁剪:如果不需要软件PWM独立调光,可以移除timer.c和相关的中断服务函数,以及led_effects.c中的复杂效果。如果只需要全局PWM调光,重点看pwm.c和main.c中的相关调用。
  4. 参数调整:根据你的LED数量和级联芯片数,修改jxi5020.h中的LED_CHANNELS和JXI5020_CASCADE_NUM宏定义。根据所需的PWM刷新频率,调整timer.c中的定时器预分频和重装载值。

这个驱动框架已经过实际项目验证,稳定可靠。它提供了从底层硬件操作到上层应用效果的完整参考,你可以像搭积木一样,根据实际需求组合使用这些模块。无论是做产品原型、毕业设计,还是参加电子竞赛,希望这份详细的解读和源码能帮你省下大量摸索的时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询