Funpack3-5基于BeagleBone Black的PRU呼吸灯设计
硬件介绍
BeagleBone Black(BBB)是一款由BeagleBoard.org推出的开源单板计算机(SBC),以其小巧的体积、低廉的价格和强大的功能而受到广泛欢迎。以下是关于BeagleBone Black的一些关键信息:
硬件特性
- 处理器:搭载了TI公司的AM3358处理器,集成了高达1GHz的ARM Cortex™ A8内核,并配备了NEON™ SIMD协处理器和SGX 530图形引擎。
- 存储:板载4GB eMMC Flash存储器和512MB DDR3 SDRAM存储器。
- 接口:包括一个HDMI D type接口、一个10/100M以太网接口(RJ45连接器)、一个高速USB 2.0 OTG接口(Mini USB B型连接器)、一个高速USB 2.0 HOST接口(USB A型连接器)、一个TF卡接口(兼容SD/MMC)、一个3线调试串口(6 pin 2.54间距连接器)等。
- 尺寸:产品尺寸为86.36 mm × 54.61mm。
工具介绍
Linux
Linux是一种开源的类Unix操作系统内核,由芬兰程序员林纳斯·托瓦兹(Linus Torvalds)于1991年首次发布,以其稳定性、安全性和灵活性而闻名。Linux内核遵循GPL(通用公共许可证)发布,允许用户自由使用、修改和分发,它支持多用户、多任务、多线程和设备驱动程序,是许多流行操作系统(如Ubuntu、Fedora、Debian等)和各种嵌入式系统的核心。下面的连接是BBB发布的linux的内核源码:
uboot
U-Boot(原名为Umbrella Boot Loader)是一个功能强大的开源引导加载程序,主要用于嵌入式系统。它负责在系统启动时初始化硬件,设置内存,执行引导配置,并最终加载Linux内核和文件系统。U-Boot提供了丰富的命令行接口,支持多种存储设备和网络协议,允许用户进行系统诊断、配置和升级操作。由于其灵活性和可扩展性,U-Boot被广泛应用于各种硬件平台,尤其是在基于ARM、MIPS和PowerPC架构的设备上。
GCC
GCC(GNU Compiler Collection)是一个开源的编译器集合,它支持多种编程语言,尤其是C和C++,被广泛用于各种操作系统和硬件平台。交叉编译工具链是一种特殊的编译环境,它允许开发者在一种系统上编译出能在另一种不同系统上运行的代码,这对于嵌入式系统开发尤为重要,因为它使得开发者可以在桌面计算机上为不同的目标硬件平台生成可执行文件。
本文使用的工具链为:
arm-gnu-toolchain-13.3.rel1-x86_64-arm-none-linux-gnueabihf
设备树
设备树是一种用于描述硬件配置的数据结构,它以树状形式组织,包含节点和属性,节点代表系统中的设备或总线,属性则描述节点的特性,如地址、中断号等。设备树被广泛用于Linux内核,特别是在嵌入式系统中,以提供一种灵活且可扩展的方式来表达硬件布局和配置,从而允许操作系统在不修改源代码的情况下支持不同的硬件平台。
以下就是BBB配置LED的设备树:
leds {
pinctrl-names = "default";
pinctrl-0 = <&user_leds_s0>;
compatible = "gpio-leds";
led2 {
label = "beaglebone:green:heartbeat";
gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
default-state = "off";
};
led3 {
label = "beaglebone:green:mmc0";
gpios = <&gpio1 22 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "mmc0";
default-state = "off";
};
led4 {
label = "beaglebone:green:usr2";
gpios = <&gpio1 23 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "cpu0";
default-state = "off";
};
led5 {
label = "beaglebone:green:usr3";
gpios = <&gpio1 24 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "mmc1";
default-state = "off";
};
};
每一个设备树对应一个LED,但是都绑定一个默认触发,后续需要取消这个默认触发。
SSH
SSH(Secure Shell)是一种网络协议,用于在不安全的网络上提供安全的远程登录、文件传输和其他网络服务。它通过加密方式保护数据传输,防止窃听、中间人攻击和其他网络攻击,确保数据的机密性和完整性。SSH客户端和服务器软件广泛用于各种操作系统,允许用户安全地访问远程服务器和执行命令,以及传输文件,而无需担心敏感信息被截获。
Makefile
Makefile是一个特殊的文件,用于在Unix和Linux系统中指导make工具自动化编译和链接程序。它包含了一系列的规则和指令,定义了如何编译源代码文件以及如何管理项目中的文件依赖关系,使得构建过程更加高效和自动化。Makefile允许开发者指定目标文件、依赖项和生成目标所需的命令,当源文件发生变化时,make工具可以根据Makefile中的规则重新构建受影响的目标,而不需要从头开始编译整个项目。
PRU
PRU = The Programmable Real‐Time Unit,在BBB的AM335X处理器中包含了2个32位的PRU,每一个PRU的运行频率是200MHz(即每一条汇编指令执行时间是5ns),这个速度堪比DSP了,另外每个PRU有8KB的instrction RAM和8KB的Data RAM,以及两个PRU之间有一块共享的12KB内存,每个PRU可以控制P8/P9上的部分IO口,可以触发一些中断,另外,BBB板子上面的512MB内存芯片是被PRU和CPU(或者称为host device)共享的,这给数据的传递带来大大的便利!总结为一句:PRU相当于一块内置于CPU中的200MHz的单片机。大致的框图如下:
设计思路
功能实现
Linux初始化一个ADC设备驱动,
一个进程初始化两个线程,一个线程读取ADC的数据,另一个线程修改PRU与linux中的共享内存数据。
PRU负责循环控制LED引脚的占空比,通过控制。根据共享内存的数据调整呼吸灯闪烁频率。
- 资源外设配置:
在使用PRU(Programmable Real-Time Unit)之前,需要先处理好资源外设的问题,包括使能相应的引脚等。 - 协作关系:
- Host device(主机设备)和PRU之间存在协作关系。
- Host device运行C程序,执行Linux系统中的任务。
- PRU执行汇编源码,完成特定的任务。
- 程序运行流程:
- C程序的运行:编写C源码,使用C编译器编译得到可执行文件,通过执行命令(如
./xxx
)在Linux系统中运行。 - PRU程序的运行:编写PRU源码(汇编代码或C源码),使用pasm汇编器编译成可执行程序,然后通过C代码将该可执行文件下载到PRU的RAM中运行。
- 示例:编写一个PRU程序用于翻转IO口,编译后存放于指定目录。编写一个C程序,其功能是将PRU可执行文件下载到PRU的instruction RAM中。运行C程序后,PRU可执行文件被下载并开始运行,即使C程序被强行退出,IO口的翻转操作仍会继续。
- C程序的运行:编写C源码,使用C编译器编译得到可执行文件,通过执行命令(如
- 通信机制:
- Host device和PRU之间需要通信,可以通过共享内存块来实现。
功能流程图
PRU
以下是根据您提供的C程序代码整理的流程图。这个流程图描述了程序的主要逻辑和步骤:
- 开始:程序的入口点。
- 定义PRU0_DRAM和pru0_dram:定义DARAM的偏移地址,并将其转换为指向unsigned int的指针。
- 定义GPIO1和变量on/off/onCount/offCount:定义GPIO1的地址,并初始化on和off的值以及它们的计数器。
- 将on/off值存储到内存:将on和off的值存储到PRU0 DRAM中。
- 清除SYSCFG[STANDBY_INIT]位:清除SYSCFG寄存器中的STANDBY_INIT位,以启用OCP主端口。
- 进入无限循环:程序进入一个无限循环,这是程序的主要控制循环。
- onCount是否为0:检查onCount是否为0。
- onCount--:如果onCount不为0,减少其值。
- 设置GPIO_SETDATAOUT,打开USR2和USR3 LED:如果onCount不为0,设置GPIO_SETDATAOUT寄存器,打开USR2和USR3 LED。
- offCount--:如果offCount不为0,减少其值。
- 设置GPIO_CLEARDATAOUT,关闭USR2和USR3 LED:如果offCount不为0,设置GPIO_CLEARDATAOUT寄存器,关闭USR2和USR3 LED。
- 重置onCount和offCount:如果onCount和offCount都为0,则从内存中重新加载on和off的值到onCount和offCount。
- 从内存中读取on/off值:从PRU0 DRAM中读取on和off的值。
- 结束:程序无限循环,直到被外部中断或复位。
Linux
以下是使用Mermaid序列图语法绘制的程序流程图:
这个序列图展示了程序的主要流程:
main
函数开始执行。- 打开
/dev/mem
设备。 - 将PRU内存映射到用户空间。
- 创建两个线程:
change_memory
和get_the_key
。 change_memory
线程进入无限循环,调用start_pwm_count
函数。get_the_key
线程进入无限循环,读取ADC值。- 主线程等待两个线程结束。
- 取消内存映射。
- 程序退出。
这个序列图简洁地展示了程序的主要逻辑和线程之间的交互。希望这能帮助你更好地理解程序的流程。如果你有任何问题或需要进一步的解释,请随时告诉我。
主要代码
启动ADC_DEVICE
首先,下载需要覆盖的设备树文件:
git clone https://github.com/beagleboard/bb.org-overlays/
cd ./bb.org-overlays/
./install.sh
然后,在
/lib/firmware
中可以找到对应的dtbo文件,用于覆盖。
修改/boot/uEnv.txt
根据您提供的eLinux.org页面内容,启用U-Boot中的capes主要涉及修改/boot/uEnv.txt
文件来配置U-Boot环境变量。以下是启用U-Boot Capes的步骤:
- 启用U-Boot覆盖层:
在/boot/uEnv.txt
文件中设置enable_uboot_overlays
变量为1来启用U-Boot覆盖层。enable_uboot_overlays=1
- 添加Cape设备树覆盖文件:
您可以通过指定设备树覆盖文件(.dtbo)的地址来添加Cape。例如:uboot_overlay_addr0=BB-ADC-00A0.dtbo
替换
uboot_overlay_addr1=<file1>.dtbo
uboot_overlay_addr2=<file2>.dtbo
uboot_overlay_addr3=<file3>.dtbo<file0>
、<file1>
等为您的.dtbo文件的实际名称。 - 禁用自动加载的Cape:
如果您需要禁用自动加载的Cape,可以使用以下选项:disable_uboot_overlay_addr0=1
disable_uboot_overlay_addr1=1
disable_uboot_overlay_addr2=1
disable_uboot_overlay_addr3=1 - 加载额外的Cape:
您可以加载更多的Cape,最多四个:uboot_overlay_addr4=<file4>.dtbo
以及一个自定义Cape:
uboot_overlay_addr5=<file5>.dtbo
uboot_overlay_addr6=<file6>.dtbo
uboot_overlay_addr7=<file7>.dtbodtb_overlay=<file8>.dtbo
- 保存并重启:
保存/boot/uEnv.txt
文件的更改,并重启设备以应用更改。 - 检查加载的覆盖层:
重启后,您可以通过运行以下命令来检查哪些覆盖层被加载:sudo ./version.sh | grep UBOOT
请注意,您需要将.dtbo文件放置在正确的目录下,通常是/lib/firmware
或/usr/lib/firmware
,这样U-Boot才能正确加载它们。此外,确保您使用的.dtbo文件与您的硬件和软件版本兼容。
如果您需要更详细的指导或有特定的Cape需要配置,请提供更多信息,以便我能提供更具体的帮助。
使用ADC
这时/sys/bus/iio/devices目录下会出现一个iio:device0目录(原本是没有的)。里面有如下内容
debian@BeagleBone:/sys/bus/iio/devices/iio:device0$ ls
buffer in_voltage0_raw in_voltage2_raw in_voltage4_raw in_voltage6_raw name power subsystem
dev in_voltage1_raw in_voltage3_raw in_voltage5_raw in_voltage7_raw of_node scan_elements uevent
in_voltage*_raw正好对应 ADC的几个引脚。
烧录PRU,并且启动PRU
#include <stdint.h>
#include <pru_cfg.h>
#include "resource_table_empty.h"
#include "prugpio.h"
#define PRU0_DRAM 0x00000 // Offset to DRAM
// Skip the first 0x200 byte of DRAM since the Makefile allocates
// 0x100 for the STACK and 0x100 for the HEAP.
volatile unsigned int *pru0_dram = (unsigned int *) (PRU0_DRAM + 0x200);
// volatile register unsigned int R30;
// volatile register unsigned int R31;
void main(void) {
int i;
uint32_t *gpio1 = (uint32_t *)GPIO1;
uint32_t on = 10;
uint32_t off = 80;
uint32_t onCount = on;
uint32_t offCount = off;
/*将数据存储在内存中*/
*pru0_dram = on;
*(pru0_dram + 1) = off;
/* Clear SYSCFG[STANDBY_INIT] to enable OCP master port */
CT_CFG.SYSCFG_bit.STANDBY_INIT = 0;
while(1){
if(onCount){
onCount--;
gpio1[GPIO_SETDATAOUT] = USR2 | USR3; // The the USR3 LED on
}
else if(offCount){
offCount--;
gpio1[GPIO_CLEARDATAOUT] = USR2 | USR3;
}else{
onCount = *pru0_dram ;
offCount = *(pru0_dram + 1);
}
}
__halt();
}
// Turns off triggers
#pragma DATA_SECTION(init_pins, ".init_pins")
#pragma RETAIN(init_pins)
const char init_pins[] =
"/sys/class/leds/beaglebone:green:usr1/trigger\0none\0" \
"/sys/class/leds/beaglebone:green:usr2/trigger\0none\0" \
"\0\0";
Makefile:
all: stop install start
@echo "MODEL = $(MODEL)"
@echo "PROC = $(PROC)"
@echo "PRUN = $(PRUN)"
@echo "PRU_DIR = $(PRU_DIR)"
# TODO: think about what we want to say if stop fails
stop:
ifneq ($(PRU_DIR),)
@echo "- Stopping PRU $(PRUN)"
@echo stop > $(PRU_DIR)/state || echo Cannot stop $(PRUN)
endif
start:
ifneq ($(PRU_DIR),)
@echo write_init_pins.sh
@$(COMMON)/write_init_pins.sh /lib/firmware/$(CHIP)-pru$(PRUN)-fw
@echo "- Starting PRU $(PRUN)"
@echo start > $(PRU_DIR)/state
else ifeq ($(PROC),tidl)
ti-mct-heap-check -c
sudo mjpg_streamer -i "input_opencv.so -r 640x480 -d /dev/$(shell fgrep -v vpe /sys/class/video4linux/video*/name | perl -ne '/\/(video\d+)\/name/ && print $$1') \
--filter ./$(TARGET)$(EXE)" -o "output_http.so -p 8090 -w /usr/share/mjpg-streamer/www"
else
./$(TARGET)$(EXE)
endif
install: $(GEN_DIR)/$(TARGET)$(EXE)
ifneq ($(PRU_DIR),)
@echo '- copying firmware file $(GEN_DIR)/$(TARGET).out to /lib/firmware/$(CHIP)-pru$(PRUN)-fw'
@cp $(GEN_DIR)/$(TARGET)$(EXE) /lib/firmware/$(CHIP)-pru$(PRUN)-fw
else
@cp $(GEN_DIR)/$(TARGET)$(EXE) ./$(TARGET)$(EXE)
endif
endif
clean:
@echo 'CLEAN'
@rm -rf $(GEN_DIR)
Linux进程
线程1:读取ADC
//读取按键的值
unsigned char ch[5]; // 读取一个字节的数据
int len = 0;
static void *get_the_key(void *arg){
int int_result = 0;
int fd;
while(1){
fd = open(SYSFS_ADC_DIR, O_RDONLY);
len = read(fd, ch, sizeof(ch) - 1); // 留一个字符给'\0'
if (len > 0) {
ch[len] = '\0'; // 确保字符串以空字符结尾
printf("%s\n", ch);
// 将字符串转换为整数
int_result = atoi(ch);
Total = int_result;
} else {
printf("Error reading from file.\n");
}
usleep(100 * 1000);
}
return (void *)0;
}
线程2:修改内存数值
int Total=100;
#define PRU_ADDR 0x4A300000 // Start of PRU memory Page 184 am335x TRM
#define PRU_LEN 0x80000 // Length of PRU memory
#define PRU0_DRAM 0x00000 // Offset to DRAM
#define PRU1_DRAM 0x02000
#define PRU_SHAREDMEM 0x10000 // Offset to shared memory
unsigned int *pru0DRAM_32int_ptr; // Points to the start of local DRAM
unsigned int *pru1DRAM_32int_ptr; // Points to the start of local DRAM
unsigned int *prusharedMem_32int_ptr; // Points to the start of the shared memory
#define SYSFS_ADC_DIR "/sys/bus/iio/devices/iio:device0/in_voltage3_raw"
/*******************************************************************************
* int start_pwm_count(int ch, int countOn, int countOff)
*
* Starts a pwm pulse on for countOn and off for countOff to a single channel (ch)
*******************************************************************************/
int start_pwm_count(unsigned int *pruDRAM_32int_ptr, int countOn, int countOff) {
countOn = countOn > 0 ? countOn : 0;
countOff = countOff > 0 ? countOff : 0;
printf("countOn: %d, countOff: %d, count: %d\n",
countOn, countOff, countOn+countOff);
// write to PRU shared memory
*pruDRAM_32int_ptr = countOn; // On time
*(pruDRAM_32int_ptr + 1) = countOff; // Off time
return 0;
}
//修改和PRU的共享内存
static void *change_memory(void *arg){
while(1){
for(int i = 0; i < Total; i++){
start_pwm_count(pru0DRAM_32int_ptr, i, Total-i);
usleep(100);
}
for(int i = Total; i > 0; i--){
start_pwm_count(pru0DRAM_32int_ptr, i, Total-i);
usleep(100);
}
}
return (void *)0;
}
功能展示
由于是通过调节旋钮来影响呼吸灯闪烁速度,几张图片无法说明,所以具体请看视频。
任务完成总结与展望
这里使用了共享内存技术,沟通异构的PRU和SOC的通信。但是由于是共享内存的原因,需要额外注意一下同步问题。未来需要进行额外的锁对内存使用进行同步。但是目前使用中,没有遇到共享内存的问题,因为整个程序流程比较简单,并且在短时间内会从内存中更新呼吸灯的闪烁。
未来还可以采用DMA的方法,进一步优化共享内存控制。