一、 项目介绍
BeagleBone Black 搭载的 AM3358 中内置了可编程实时单元(Programmable Real-time Unit, PRU)为实时控制提供了强大的支持,本项目旨在充分利用PRU的低延迟特性,结合网页控制接口,实现一个可以自定义控制的LED呼吸灯系统。
项目目标:
- 使用BeagleBone Black的PRU实现LED的呼吸灯效果,通过控制PWM输出改变LED的亮度,模拟“呼吸”效果。
- 配置一个物理按键,实现按键按下后切换LED呼吸灯的闪烁速度(例如慢速、中速、快速等不同频率)。
- 构建一个简单的网页控制界面,通过该网页可以控制PRU的内存,从而实现对LED状态(开/关)和闪烁速度的实时控制。
- 综合上述目标,实现同时使用按钮和网页控制LED闪烁速度。
二、设计思路
任务一:点灯!PRU控制LED呼吸灯
使用pru控制LED的亮灭,通过占空比的变化来模拟呼吸灯效果。通过PWM模拟多种函数波形达到不同的呼吸灯效果,计算占空比值时,可选择以下函数曲线,例如正弦函数、反Gamma曲线等,调整LED亮度变化的平滑程度。同时需要在pru操作LED的间隙检测按钮状态,并在按下和释放检测中进行消抖处理。
根据传入的参数,检测按钮是否被按下或释放同时之前没有被触发过,同时进行2ms的消抖,释放按钮后使用下一种模式。
根据传入的当前循环数、总半周期数和使用的模式,计算当前的占空比并映射到查找表。过多的复杂数学计算可能会导致最终生成的程序超过pru内存限制导致编译失败,预计算的查找表可以提升运行速度。
软件流程图:
任务二:在系统中建立一个网页,并且与LED联动,使用网线连接到设备上时,可以从网页中控制LED的开关与闪烁
AM335X的PRU具有两个独立的处理单元,每个处理单元有独立的内存区域但可互相访问,还有一个共享的内存区域,可以被linux系统读写。利用这个特性,通过python的flask库实现前端读取传入的指令,并通过” /dev/mem”改写指定的共享内存地址,pru程序读取到内存数据后改变闪烁速率或模式。
软件流程图:
任务三:
综合任务二和三,使用pru控制LED的亮灭,通过占空比的变化来模拟呼吸灯效果,同时需要在pru操作LED的间隙检测按钮状态,并在按下和释放检测中进行消抖处理,以及读取pru共享内存,检测模式和速度参数是否改变。通过python的flask库实现前端,根据传入的http指令读取或改写pru内存,从而控制小灯。
软件流程图:
呼吸灯效果:
网页端与按钮同时控制效果,按下按钮或网页端切换模式:
网页前端设置每0.5s获取一次状态数据并更新显示
三、实现过程
1.配置开发环境
通过config-pin查询需要使用的pin是否被其它功能占用(如:config-pin -i p9.28
),默认设置下,HDMI和AUDIO功能占用了对应的pin,无法更改为PRU功能,需要在"/boot/uEnv.txt"中取消disable的注释。
如果编译环境有问题,在ti官网下载pru cgt安装包 https://www.ti.com/tool/PRU-CGT#downloads
2. 可以通过三种方法设置网络:
a.通过usb连接电脑可以使用电脑代理来连接网络,导出proxy环境设置,无需其它配置,如:
export https_proxy=https://192.168.7.1:10809
export http_proxy=https://192.168.7.1:10809
b.设置网关以及网络分享:
在bbb上设置默认网关为usb(电脑),sudo ip route add default via 192.168.7.1,或修改"/etc/systemd/network/usb0.network",在[Network]下添加Gateway=192.168.7.1,持久化网络配置。
在电脑中将正在使用的网络共享给和bbb通过usb连接的网络并且设置与bbb连接的网络,确保IP地址配置正确
c.直接使用网线连接
3. 连接bbb和LED、按钮
正确连接电源,LED(#22绿/#32红)和按钮(#13)集成在X-STM32MP-MSP01拓展板上,按钮和LED处于高电平,使用时需注意电流方向。连接的两个pru接口,一个输入PRU0_R31_16(P9.28)连接按钮,另一个输出PRU0_R30_5(P9.27)连接LED。
连接完成后需要设置连接的pin为对应的功能,如:
config-pin p9.27 pruout //led灯
config-pin p9.28 pruin //按钮
4. 具体代码实现
4.1 循环延时函数
void delayCycle(uint32_t cycle) {
uint32_t step = 0;
for (step = 0; step < cycle; step++) {
__delay_cycles(2000); //about 0.01ms
}
}
__delay_cycles()函数只支持常数参数,通过循环控制占空时长。
4.2 按钮状态检查
void check(uint8_t press)
{
if(press){
if (!(__R31 && button) && (!buttonState)) {
// Button pressed, debounce
__delay_cycles(1000000); // 5ms debounce at 200MHz
if (!(__R31 && button)) {
// Confirmed press, change frequency
buttonState = 1;
}
}
}
else{
if (((__R31 && button)) && buttonState) {
// Button released, debounce
__delay_cycles(1000000); // 5ms debounce at 200MHz
if (__R31 && button) {
buttonState = 0;
(*mode_mem) ++;
if (*mode_mem == 5) {*mode_mem = 0; }
}
}
}
}
检测按钮是否被按下或释放,并进行5ms的除抖。只有当检测到按钮被按下且之前没有被按下,改写按钮状态为1;检测到按钮之前没有被按下(按钮状态为1),且被释放,改写按钮状态为0并增加模式值,增加模式值到5时改写为0返回。
4.3 占空比查表
uint8_t duty(uint32_t z, uint32_t period_rd, uint8_t algo)
{
float ratio = (float)z / period_rd;
uint8_t duty = (uint8_t)(ratio * 255);
if(algo == 2){ // sinf(0.5 * PI * z / period_rd)
duty = sin_lut[duty];
}
if(algo == 3){ // 1 - cosf(0.5 * PI * z / period_rd)
duty = sinr_lut[duty];
}
if(algo == 4){ // gamma2.2
duty = gamma_lut[duty];
}
return duty;
}
根据传入的模式查询并计算占空时长。
4.4 pwm函数
void pwm(uint32_t period_rd, uint8_t mode)
{
uint32_t z = 0;
uint8_t duty0;
for (z=0; z<period_rd; z++) {
__R30 &= ~led;
duty0 = duty(z, period_rd, *mode_mem);
delayCycle(duty0);
__R30 ^= led;
delayCycle(255 - duty0);
check(z % 2);
if (*mode_mem==0) {return;}
}
for (z=0; z<period_rd; z++) {
__R30 |= led;
duty0 = duty(z, period_rd, *mode_mem);
delayCycle(duty0);
__R30 ^= led;
delayCycle(255 - duty0);
check(z % 2);
if (*mode_mem==0) {return;}
}
}
根据传入的参数对LED进行pwm控制,交替检测按钮状态,检测到模式值为0时返回。
4.5 网页后端实现
使用flask库作为后端接收客户端的请求,/get_current_value 接受前端的 GET 请求,返回PRU共享内存中当前的模式值和轮次值;/update_mem:接受前端的 POST 请求,用于更新PRU共享内存中的模式值或轮次值;使用 mmap 通过 /dev/mem 直接读写PRU共享内存。 GET 请求 "http://192.168.7.2:9000" 时,返回 static 目录下的index.html
from flask import Flask, request, jsonify
import mmap
import os
app = Flask(__name__)
PRU_SHARED_MEM_ADDR_MODE = 0x00010000
PRU_SHARED_MEM_ADDR_ROUND = 0x00010004
PRU_LEN = 0x80000
PRU_ADDR = 0x4A300000
def read_shared_memory(address):
with open("/dev/mem", "r+b") as f:
mem = mmap.mmap(f.fileno(), PRU_LEN, offset=PRU_ADDR)
mem.seek(address)
value = int.from_bytes(mem.read(4), byteorder='little')
mem.close()
return value
def write_shared_memory(address, value):
with open("/dev/mem", "r+b") as f:
mem = mmap.mmap(f.fileno(), PRU_LEN, offset=PRU_ADDR)
mem.seek(address)
mem.write(value.to_bytes(4, byteorder='little'))
mem.close()
@app.route('/update_mem', methods=['POST'])
def update_mem():
try:
mode = request.form.get('mode')
round_delta = request.form.get('round')
if mode is not None:
mode = int(mode)
if not (0 <= mode <= 4):
return jsonify({"error": "Mode value must be between 0 and 4"}), 400
current_mode = read_shared_memory(PRU_SHARED_MEM_ADDR_MODE)
write_shared_memory(PRU_SHARED_MEM_ADDR_MODE, mode)
return jsonify({
"message": "Mode updated",
"previous_mode": current_mode,
"new_mode": mode
})
if round_delta is not None:
round_delta = int(round_delta)
if round_delta not in [-1, 1]:
return jsonify({"error": "Round delta must be -1 or 1"}), 400
current_round = read_shared_memory(PRU_SHARED_MEM_ADDR_ROUND)
new_round = current_round + round_delta
if not (0 <= new_round <= 4):
return jsonify({"error": "Round value out of range. Must remain between 0 and 4."}), 400
write_shared_memory(PRU_SHARED_MEM_ADDR_ROUND, new_round)
return jsonify({
"message": "Round updated",
"previous_round": current_round,
"new_round": new_round
})
return jsonify({"error": "No valid data provided. Specify either 'mode' or 'round'."}), 400
except ValueError:
return jsonify({"error": "Invalid data provided. Mode must be an integer, and Round delta must be -1 or 1."}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/')
def index():
return app.send_static_file('index.html')
@app.route('/get_current_value', methods=['GET'])
def get_current_value():
try:
current_mode = read_shared_memory(PRU_SHARED_MEM_ADDR_MODE)
current_round = read_shared_memory(PRU_SHARED_MEM_ADDR_ROUND)
return jsonify({"current_mode": current_mode, "current_round": current_round})
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000)
4.6 运行
编译好固件并加载到pru中启动:
make
make install_PRU0
如果有问题,手动输入命令,注意替换生成的固件位置:
echo stop | sudo tee /sys/class/remoteproc/remoteproc1/state //停止PRU0运行
make clean
make
sudo cp /root/code/ledUltimate/gen/ledUltimate.out /lib/firmware/am335x-pru0-fw //加载固件到PRU0
echo start | sudo tee /sys/class/remoteproc/remoteproc1/state //运行PRU0
运行后端:
source /root/code/venv/bin/activate //加载安装好相关库的python虚拟环境
python3 /root/code/venv/web.py //运行后端
网页每隔0.5s获取一次内存数据,
四、未来的计划建议
PRU还有许多有意思的特性有待发掘,如还可以通过RPMsg使PRU和Linux内核的双向通信;同时使用两个pru核心并协作完成复杂任务;工业级实时通信等。