Funpack3-5 基于BeagleBone® Black的LED呼吸灯
BeagleBone® Black是一款基于TI AM3358的嵌入式开发板,该芯片的原理框图如下图所示。该芯片包含一颗主频高达1GHz的ARM Cortex-A8内核与两颗可编程实时处理器(Programmable Real-Time Units,PRU),还包括独立的显示单元与众多外设。借助PRU的实时特性,使得其非常适合用做工业领域的控制与协议栈实现,以实现对工业以太网(Profinet、EtherCAT、EtherNET等协议)的接入。
本设计拟通过AM3358的PRU,通过读写GPIO来实现按键控制呼吸灯速度的功能。开发平台直接使用Cloud9,其中按键输入为P9-30,呼吸灯输出为P9-27,程序运行在PRU0上。下面将详细该项目的具体实现过程,包含硬件上按键与LED的电路搭建,与软件上对PRU IO读写与呼吸灯控制逻辑的实现。
硬件
参考BeagleBone Black开发板原理图可知,该开发板有P8 P9两个扩展排座。其中P8端口多被板卡上的LCD、MMC等外设复用,由于需要使用PRU0进行控制,因此在P9上选取两个GPIO端口用于读写。
参考开发板手册P69页的Table7.2 Expansion Header P9 Pinout,选取P9-27作为输出,对应的PRU0端口为pr1_pru0_pru_r30_5。同样地选取P9-30为输入,对应的PRU0端口为pr1_pru0_pru_r31_2。在后续的软件代码中将会将这两个寄存器映通过宏定义的方式,定义到P9-27/30。
其中P9-27连接到LED灯,GPIO输出高电平时点亮;P9-30连接到按键,按键没有按下时通过上拉电阻拉低,为低电平。按键按下后端口上的电压为R2 R3对VCC的分压,为高电平。
软件
软件部分的实现主要分为两个部分,即呼吸灯的实现与控制逻辑。其中呼吸灯的实现主要通过可变PWM占空比来实现亮度的渐变,借助软件延迟实现占空比可调。控制逻辑包括按键的读取与呼吸灯速度参数的改变。软件的具体流程图如下图所示:
对PRU而言,GPIO的操作本质上是对R30(输出)与R31寄存器的读写。SDK在prugpio.h中定义了扩展端口P9上IO与PRU相关寄存器的对应关系,其中P9-27对应的是PRU0的Bit5,P9-30对应的是PRU0的Bit2。在进行读写操作时只需要将R30/R31与该值相与,即可提取出该端口上的IO电平。
// R30/R31 output bits on pru0
#define P9_31 (1<<0)
#define P9_29 (1<<1)
#define P9_30 (1<<2)
#define P9_28 (1<<3)
#define P9_92 (1<<4)
#define P9_27 (1<<5)
#define P9_91 (1<<6)
#define P9_25 (1<<7)
呼吸灯通过可变PWM占空比实现,当PWM频率高到肉眼无法察觉时便可以忽略开关闪烁。当占空比越高时灯越亮,占空比越低灯越暗,叠加变暗与变亮两个过程就可以实现连续的呼吸灯效果,其具体过程的示意图如下所示。
具体代码描述如下,该函数传入一个speed用来控制流水灯的速度。在第一个循环时,PWM占空比随着变量i的累加逐渐变小,灯由亮变暗。第二个循环时变量递减,灯由暗变亮。
// breath led, the speed range is 1~10
void breath_led(int speed) {
uint32_t i;
for (i = 0; i < 1000; i++) {
__R30 |= led; // LED -> 1
delay((1000-i)/speed);
__R30 &= ~led; // LED -> 0
delay(i/speed);
}
for (i = 1000; i > 0; i--) {
__R30 |= led; // LED -> 1
delay((1000-i)/speed);
__R30 &= ~led; // LED -> 0
delay(i/speed);
}
}
其中延迟函数如下所示,借助内联汇编__asm__(" nop");
使用软件延迟实现,一个nop指令对应一个时钟周期的长度。考虑到涉及循环周期的计算,实际的延迟时间略大于设定时间。
// delay by nop
void delay(uint32_t delay_us) {
uint32_t i;
double cycle_time_s = 1.0 / (double)PRU_CLK_FREQ;
// us -> s
double delay_s = (double)delay_us * 1e-6;
// cal the clock cycles
uint32_t cycle_count = (uint32_t)(delay_s / cycle_time_s);
// delay by one clock cycle
for (i = 0; i < cycle_count; i++) {
__asm__(" nop");
}
}
控制逻辑包含对按键的读取与流水灯参数的下发,当按键按下时R31对应的Bit位置高,此时保存流水灯速度的变量speed_level以1为步进累加,最后将速度参数传入流水灯函数以控制速度。
while(1) {
if(__R31 & key){
speed_level ++;
if(speed_level > 3){
speed_level = 1;
}
}
breath_led(speed_level*3);
}
总结
AM3358是工业界较为常用的一款处理器,Coretx A8+PRU的组合使得其可以兼顾高性能与高实时性,非常适合将对实时性要求较高的任务卸载到PRU以提高响应速率,传统任务继续使用高性能Coretx A8来运行以提高效率。一个典型的引用场景是工业现场PLC设备的组网,TI官方提供了运行在PRU上的Profinet/EtherCAT协议栈,非常适合进行实时的机械控制。
在本项目中,我们有机会借助BeagleBone Black来一窥AM3358上PRU的开发流程,为后续应用的选型评估提供了相当有价值的参考。
附录
////////////////////////////////////////
// blinkR30.pru0.c
// Blinks LEDs wired to P9_29 (and others) by writing register R30 on the PRU
// Wiring: P9_29 connects to the plus lead of an LED. The negative lead of the
// LED goes to a 220 Ohm resistor. The other lead of the resistor goes
// to ground.
// Setup: config-pin P9_27 pruout config-pin P9_30 pruin
// See: prugpio.h to see which pins attach to R30
// PRU: pru0
////////////////////////////////////////
#include <stdint.h>
#include <pru_cfg.h>
#include "resource_table_empty.h"
#include "prugpio.h"
#define PRU_CLK_FREQ 200000000
volatile register unsigned int __R30;
volatile register unsigned int __R31;
uint32_t led = P9_27;
uint32_t key = P9_30;
uint32_t speed_level = 1;
// delay by nop
void delay(uint32_t delay_us) {
uint32_t i;
double cycle_time_s = 1.0 / (double)PRU_CLK_FREQ;
// 将微秒转换为秒
double delay_s = (double)delay_us * 1e-6;
// 算出需要的时钟周期数
uint32_t cycle_count = (uint32_t)(delay_s / cycle_time_s);
// 使用nop实现延迟
for (i = 0; i < cycle_count; i++) {
__asm__(" nop");
}
}
// breath led, the speed range is 1~10
void breath_led(int speed) {
uint32_t i;
for (i = 0; i < 1000; i++) {
__R30 |= led; // LED -> 1
delay((1000-i)/speed);
__R30 &= ~led; // LED -> 0
delay(i/speed);
}
for (i = 1000; i > 0; i--) {
__R30 |= led; // LED -> 1
delay((1000-i)/speed);
__R30 &= ~led; // LED -> 0
delay(i/speed);
}
}
void main(void) {
// Clear SYSCFG[STANDBY_INIT] to enable OCP master port
CT_CFG.SYSCFG_bit.STANDBY_INIT = 0;
while(1) {
if(__R31 & key){
speed_level ++;
if(speed_level > 3){
speed_level = 1;
}
}
breath_led(speed_level*3);
}
__halt();
}
// Sets pinmux
#pragma DATA_SECTION(init_pins, ".init_pins")
#pragma RETAIN(init_pins)
const char init_pins[] =
"/sys/devices/platform/ocp/ocp:P9_27_pinmux/state\0pruout\0" \
"/sys/devices/platform/ocp/ocp:P9_30_pinmux/state\0pruin\0" \
"\0\0";