starting LedBar for rasperry pi via smi (C)
This commit is contained in:
parent
5c577897b2
commit
a01770282d
52
.gitignore
vendored
52
.gitignore
vendored
@ -6,6 +6,58 @@ __pycache__/
|
|||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
# Prerequisites
|
||||||
|
*.d
|
||||||
|
|
||||||
|
# Object files
|
||||||
|
*.o
|
||||||
|
*.ko
|
||||||
|
*.obj
|
||||||
|
*.elf
|
||||||
|
|
||||||
|
# Linker output
|
||||||
|
*.ilk
|
||||||
|
*.map
|
||||||
|
*.exp
|
||||||
|
|
||||||
|
# Precompiled Headers
|
||||||
|
*.gch
|
||||||
|
*.pch
|
||||||
|
|
||||||
|
# Libraries
|
||||||
|
*.lib
|
||||||
|
*.a
|
||||||
|
*.la
|
||||||
|
*.lo
|
||||||
|
|
||||||
|
# Shared objects (inc. Windows DLLs)
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.so.*
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Executables
|
||||||
|
*.exe
|
||||||
|
*.out
|
||||||
|
*.app
|
||||||
|
*.i*86
|
||||||
|
*.x86_64
|
||||||
|
*.hex
|
||||||
|
|
||||||
|
# Debug files
|
||||||
|
*.dSYM/
|
||||||
|
*.su
|
||||||
|
*.idb
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# Kernel Module Compile Results
|
||||||
|
*.mod*
|
||||||
|
*.cmd
|
||||||
|
.tmp_versions/
|
||||||
|
modules.order
|
||||||
|
Module.symvers
|
||||||
|
Mkfile.old
|
||||||
|
dkms.conf
|
||||||
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
|
40
.vscode/tasks.json
vendored
Normal file
40
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "cppbuild",
|
||||||
|
"label": "Build project",
|
||||||
|
"command": "make",
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": [
|
||||||
|
"$gcc"
|
||||||
|
],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"detail": "compiler: /usr/bin/gcc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "cppbuild",
|
||||||
|
"label": "C/C++: gcc build active file",
|
||||||
|
"command": "/usr/bin/gcc",
|
||||||
|
"args": [
|
||||||
|
"-g",
|
||||||
|
"${file}",
|
||||||
|
"-o",
|
||||||
|
"${fileDirname}/${fileBasenameNoExtension}"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": [
|
||||||
|
"$gcc"
|
||||||
|
],
|
||||||
|
"group": "build",
|
||||||
|
"detail": "compiler: /usr/bin/gcc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
23
RpiLedBars/Makefile
Normal file
23
RpiLedBars/Makefile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
CC=gcc
|
||||||
|
CFLAGS=-Wall -g -DDEBUG
|
||||||
|
LDFLAGS=#-lpthread
|
||||||
|
SRCDIR=src
|
||||||
|
OBJDIR=obj
|
||||||
|
BINDIR=bin
|
||||||
|
SRC=$(notdir $(wildcard $(SRCDIR)/*.c))
|
||||||
|
OBJ=$(SRC:.c=.o)
|
||||||
|
BIN=pixled
|
||||||
|
|
||||||
|
all: $(addprefix $(BINDIR)/, $(BIN))
|
||||||
|
|
||||||
|
$(OBJDIR)/%.o: $(SRCDIR)/%.c
|
||||||
|
if [ ! -d $(OBJDIR) ]; then mkdir "$(OBJDIR)"; fi
|
||||||
|
$(CC) -c -o $@ $< $(CFLAGS)
|
||||||
|
|
||||||
|
$(BINDIR)/$(BIN) : $(addprefix $(OBJDIR)/, $(OBJ))
|
||||||
|
if [ ! -d "$(BINDIR)" ]; then mkdir "$(BINDIR)"; fi
|
||||||
|
$(CC) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BINDIR)/* $(OBJDIR)/*
|
||||||
|
rmdir $(BINDIR) $(OBJDIR)
|
346
RpiLedBars/src/rpi_dma_utils.c
Normal file
346
RpiLedBars/src/rpi_dma_utils.c
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
// Raspberry Pi DMA utilities; see https://iosoft.blog for details
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020 Jeremy P Bentham
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
|
||||||
|
#include "rpi_dma_utils.h"
|
||||||
|
|
||||||
|
// If non-zero, print debug information
|
||||||
|
#define DEBUG 0
|
||||||
|
// If non-zero, enable PWM hardware output
|
||||||
|
#define PWM_OUT 0
|
||||||
|
|
||||||
|
char *dma_regstrs[] = {"DMA CS", "CB_AD", "TI", "SRCE_AD", "DEST_AD",
|
||||||
|
"TFR_LEN", "STRIDE", "NEXT_CB", "DEBUG", ""};
|
||||||
|
char *gpio_mode_strs[] = {GPIO_MODE_STRS};
|
||||||
|
|
||||||
|
// Virtual memory pointers to acceess GPIO, DMA and PWM from user space
|
||||||
|
MEM_MAP pwm_regs, gpio_regs, dma_regs, clk_regs;
|
||||||
|
|
||||||
|
// Use mmap to obtain virtual address, given physical
|
||||||
|
void *map_periph(MEM_MAP *mp, void *phys, int size)
|
||||||
|
{
|
||||||
|
mp->phys = phys;
|
||||||
|
mp->size = PAGE_ROUNDUP(size);
|
||||||
|
mp->bus = (void *)((uint32_t)phys - PHYS_REG_BASE + BUS_REG_BASE);
|
||||||
|
mp->virt = map_segment(phys, mp->size);
|
||||||
|
return(mp->virt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate uncached memory, get bus & phys addresses
|
||||||
|
void *map_uncached_mem(MEM_MAP *mp, int size)
|
||||||
|
{
|
||||||
|
void *ret;
|
||||||
|
mp->size = PAGE_ROUNDUP(size);
|
||||||
|
mp->fd = open_mbox();
|
||||||
|
ret = (mp->h = alloc_vc_mem(mp->fd, mp->size, DMA_MEM_FLAGS)) > 0 &&
|
||||||
|
(mp->bus = lock_vc_mem(mp->fd, mp->h)) != 0 &&
|
||||||
|
(mp->virt = map_segment(BUS_PHYS_ADDR(mp->bus), mp->size)) != 0
|
||||||
|
? mp->virt : 0;
|
||||||
|
printf("VC mem handle %u, phys %p, virt %p\n", mp->h, mp->bus, mp->virt);
|
||||||
|
return(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free mapped peripheral or memory
|
||||||
|
void unmap_periph_mem(MEM_MAP *mp)
|
||||||
|
{
|
||||||
|
if (mp)
|
||||||
|
{
|
||||||
|
if (mp->fd)
|
||||||
|
{
|
||||||
|
unmap_segment(mp->virt, mp->size);
|
||||||
|
unlock_vc_mem(mp->fd, mp->h);
|
||||||
|
free_vc_mem(mp->fd, mp->h);
|
||||||
|
close_mbox(mp->fd);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
unmap_segment(mp->virt, mp->size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- GPIO -----
|
||||||
|
|
||||||
|
// Set input or output with pullups
|
||||||
|
void gpio_set(int pin, int mode, int pull)
|
||||||
|
{
|
||||||
|
gpio_mode(pin, mode);
|
||||||
|
gpio_pull(pin, pull);
|
||||||
|
}
|
||||||
|
// Set I/P pullup or pulldown
|
||||||
|
void gpio_pull(int pin, int pull)
|
||||||
|
{
|
||||||
|
volatile uint32_t *reg = REG32(gpio_regs, GPIO_GPPUDCLK0) + pin / 32;
|
||||||
|
|
||||||
|
*REG32(gpio_regs, GPIO_GPPUD) = pull;
|
||||||
|
usleep(2);
|
||||||
|
*reg = pin << (pin % 32);
|
||||||
|
usleep(2);
|
||||||
|
*REG32(gpio_regs, GPIO_GPPUD) = 0;
|
||||||
|
*reg = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set input or output
|
||||||
|
void gpio_mode(int pin, int mode)
|
||||||
|
{
|
||||||
|
volatile uint32_t *reg = REG32(gpio_regs, GPIO_MODE0) + pin / 10, shift = (pin % 10) * 3;
|
||||||
|
*reg = (*reg & ~(7 << shift)) | (mode << shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set an O/P pin
|
||||||
|
void gpio_out(int pin, int val)
|
||||||
|
{
|
||||||
|
volatile uint32_t *reg = REG32(gpio_regs, val ? GPIO_SET0 : GPIO_CLR0) + pin/32;
|
||||||
|
*reg = 1 << (pin % 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an I/P pin value
|
||||||
|
uint8_t gpio_in(int pin)
|
||||||
|
{
|
||||||
|
volatile uint32_t *reg = REG32(gpio_regs, GPIO_LEV0) + pin/32;
|
||||||
|
return (((*reg) >> (pin % 32)) & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the values in a GPIO mode register
|
||||||
|
void disp_mode_vals(uint32_t mode)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i=0; i<10; i++)
|
||||||
|
printf("%u:%-4s ", i, gpio_mode_strs[(mode>>(i*3)) & 7]);
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- VIDEOCORE MAILBOX -----
|
||||||
|
|
||||||
|
// Open mailbox interface, return file descriptor
|
||||||
|
int open_mbox(void)
|
||||||
|
{
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
if ((fd = open("/dev/vcio", 0)) < 0)
|
||||||
|
fail("Error: can't open VC mailbox\n");
|
||||||
|
return(fd);
|
||||||
|
}
|
||||||
|
// Close mailbox interface
|
||||||
|
void close_mbox(int fd)
|
||||||
|
{
|
||||||
|
if (fd >= 0)
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message to mailbox, return first response int, 0 if error
|
||||||
|
uint32_t msg_mbox(int fd, VC_MSG *msgp)
|
||||||
|
{
|
||||||
|
uint32_t ret=0, i;
|
||||||
|
|
||||||
|
for (i=msgp->dlen/4; i<=msgp->blen/4; i+=4)
|
||||||
|
msgp->uints[i++] = 0;
|
||||||
|
msgp->len = (msgp->blen + 6) * 4;
|
||||||
|
msgp->req = 0;
|
||||||
|
if (ioctl(fd, _IOWR(100, 0, void *), msgp) < 0)
|
||||||
|
printf("VC IOCTL failed\n");
|
||||||
|
else if ((msgp->req&0x80000000) == 0)
|
||||||
|
printf("VC IOCTL error\n");
|
||||||
|
else if (msgp->req == 0x80000001)
|
||||||
|
printf("VC IOCTL partial error\n");
|
||||||
|
else
|
||||||
|
ret = msgp->uints[0];
|
||||||
|
#if DEBUG
|
||||||
|
disp_vc_msg(msgp);
|
||||||
|
#endif
|
||||||
|
return(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate memory on PAGE_SIZE boundary, return handle
|
||||||
|
uint32_t alloc_vc_mem(int fd, uint32_t size, VC_ALLOC_FLAGS flags)
|
||||||
|
{
|
||||||
|
VC_MSG msg={.tag=0x3000c, .blen=12, .dlen=12,
|
||||||
|
.uints={PAGE_ROUNDUP(size), PAGE_SIZE, flags}};
|
||||||
|
return(msg_mbox(fd, &msg));
|
||||||
|
}
|
||||||
|
// Lock allocated memory, return bus address
|
||||||
|
void *lock_vc_mem(int fd, int h)
|
||||||
|
{
|
||||||
|
VC_MSG msg={.tag=0x3000d, .blen=4, .dlen=4, .uints={h}};
|
||||||
|
return(h ? (void *)msg_mbox(fd, &msg) : 0);
|
||||||
|
}
|
||||||
|
// Unlock allocated memory
|
||||||
|
uint32_t unlock_vc_mem(int fd, int h)
|
||||||
|
{
|
||||||
|
VC_MSG msg={.tag=0x3000e, .blen=4, .dlen=4, .uints={h}};
|
||||||
|
return(h ? msg_mbox(fd, &msg) : 0);
|
||||||
|
}
|
||||||
|
// Free memory
|
||||||
|
uint32_t free_vc_mem(int fd, int h)
|
||||||
|
{
|
||||||
|
VC_MSG msg={.tag=0x3000f, .blen=4, .dlen=4, .uints={h}};
|
||||||
|
return(h ? msg_mbox(fd, &msg) : 0);
|
||||||
|
}
|
||||||
|
uint32_t set_vc_clock(int fd, int id, uint32_t freq)
|
||||||
|
{
|
||||||
|
VC_MSG msg1={.tag=0x38001, .blen=8, .dlen=8, .uints={id, 1}};
|
||||||
|
VC_MSG msg2={.tag=0x38002, .blen=12, .dlen=12, .uints={id, freq, 0}};
|
||||||
|
msg_mbox(fd, &msg1);
|
||||||
|
disp_vc_msg(&msg1);
|
||||||
|
msg_mbox(fd, &msg2);
|
||||||
|
disp_vc_msg(&msg2);
|
||||||
|
return(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display mailbox message
|
||||||
|
void disp_vc_msg(VC_MSG *msgp)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
printf("VC msg len=%X, req=%X, tag=%X, blen=%x, dlen=%x, data ",
|
||||||
|
msgp->len, msgp->req, msgp->tag, msgp->blen, msgp->dlen);
|
||||||
|
for (i=0; i<msgp->blen/4; i++)
|
||||||
|
printf("%08X ", msgp->uints[i]);
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- VIRTUAL MEMORY -----
|
||||||
|
|
||||||
|
// Get virtual memory segment for peripheral regs or physical mem
|
||||||
|
void *map_segment(void *addr, int size)
|
||||||
|
{
|
||||||
|
int fd;
|
||||||
|
void *mem;
|
||||||
|
|
||||||
|
size = PAGE_ROUNDUP(size);
|
||||||
|
if ((fd = open ("/dev/mem", O_RDWR|O_SYNC|O_CLOEXEC)) < 0)
|
||||||
|
fail("Error: can't open /dev/mem, run using sudo\n");
|
||||||
|
mem = mmap(0, size, PROT_WRITE|PROT_READ, MAP_SHARED, fd, (uint32_t)addr);
|
||||||
|
close(fd);
|
||||||
|
#if DEBUG
|
||||||
|
printf("Map %p -> %p\n", (void *)addr, mem);
|
||||||
|
#endif
|
||||||
|
if (mem == MAP_FAILED)
|
||||||
|
fail("Error: can't map memory\n");
|
||||||
|
return(mem);
|
||||||
|
}
|
||||||
|
// Free mapped memory
|
||||||
|
void unmap_segment(void *mem, int size)
|
||||||
|
{
|
||||||
|
if (mem)
|
||||||
|
munmap(mem, PAGE_ROUNDUP(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- DMA -----
|
||||||
|
|
||||||
|
// Enable and reset DMA
|
||||||
|
void enable_dma(int chan)
|
||||||
|
{
|
||||||
|
*REG32(dma_regs, DMA_ENABLE) |= (1 << chan);
|
||||||
|
*REG32(dma_regs, DMA_REG(chan, DMA_CS)) = 1 << 31;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start DMA, given first control block
|
||||||
|
void start_dma(MEM_MAP *mp, int chan, DMA_CB *cbp, uint32_t csval)
|
||||||
|
{
|
||||||
|
*REG32(dma_regs, DMA_REG(chan, DMA_CONBLK_AD)) = MEM_BUS_ADDR(mp, cbp);
|
||||||
|
*REG32(dma_regs, DMA_REG(chan, DMA_CS)) = 2; // Clear 'end' flag
|
||||||
|
*REG32(dma_regs, DMA_REG(chan, DMA_DEBUG)) = 7; // Clear error bits
|
||||||
|
*REG32(dma_regs, DMA_REG(chan, DMA_CS)) = 1|csval; // Start DMA
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return remaining transfer length
|
||||||
|
uint32_t dma_transfer_len(int chan)
|
||||||
|
{
|
||||||
|
return(*REG32(dma_regs, DMA_REG(chan, DMA_TXFR_LEN)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if DMA is active
|
||||||
|
uint32_t dma_active(int chan)
|
||||||
|
{
|
||||||
|
return((*REG32(dma_regs, DMA_REG(chan, DMA_CS))) & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Halt current DMA operation by resetting controller
|
||||||
|
void stop_dma(int chan)
|
||||||
|
{
|
||||||
|
if (dma_regs.virt)
|
||||||
|
*REG32(dma_regs, DMA_REG(chan, DMA_CS)) = 1 << 31;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display DMA registers
|
||||||
|
void disp_dma(int chan)
|
||||||
|
{
|
||||||
|
volatile uint32_t *p=REG32(dma_regs, DMA_REG(chan, DMA_CS));
|
||||||
|
int i=0;
|
||||||
|
|
||||||
|
while (dma_regstrs[i][0])
|
||||||
|
{
|
||||||
|
printf("%-7s %08X ", dma_regstrs[i++], *p++);
|
||||||
|
if (i%5==0 || dma_regstrs[i][0]==0)
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- PWM -----
|
||||||
|
|
||||||
|
// Initialise PWM
|
||||||
|
void init_pwm(int freq, int range, int val)
|
||||||
|
{
|
||||||
|
stop_pwm();
|
||||||
|
if (*REG32(pwm_regs, PWM_STA) & 0x100)
|
||||||
|
{
|
||||||
|
printf("PWM bus error\n");
|
||||||
|
*REG32(pwm_regs, PWM_STA) = 0x100;
|
||||||
|
}
|
||||||
|
#if USE_VC_CLOCK_SET
|
||||||
|
set_vc_clock(mbox_fd, PWM_CLOCK_ID, freq);
|
||||||
|
#else
|
||||||
|
int divi=CLOCK_HZ / freq;
|
||||||
|
*REG32(clk_regs, CLK_PWM_CTL) = CLK_PASSWD | (1 << 5);
|
||||||
|
while (*REG32(clk_regs, CLK_PWM_CTL) & (1 << 7)) ;
|
||||||
|
*REG32(clk_regs, CLK_PWM_DIV) = CLK_PASSWD | (divi << 12);
|
||||||
|
*REG32(clk_regs, CLK_PWM_CTL) = CLK_PASSWD | 6 | (1 << 4);
|
||||||
|
while ((*REG32(clk_regs, CLK_PWM_CTL) & (1 << 7)) == 0) ;
|
||||||
|
#endif
|
||||||
|
usleep(100);
|
||||||
|
*REG32(pwm_regs, PWM_RNG1) = range;
|
||||||
|
*REG32(pwm_regs, PWM_FIF1) = val;
|
||||||
|
#if PWM_OUT
|
||||||
|
gpio_mode(PWM_PIN, PWM_PIN==12 ? GPIO_ALT0 : GPIO_ALT5);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start PWM operation
|
||||||
|
void start_pwm(void)
|
||||||
|
{
|
||||||
|
*REG32(pwm_regs, PWM_CTL) = PWM_CTL_USEF1 | PWM_ENAB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop PWM operation
|
||||||
|
void stop_pwm(void)
|
||||||
|
{
|
||||||
|
if (pwm_regs.virt)
|
||||||
|
{
|
||||||
|
*REG32(pwm_regs, PWM_CTL) = 0;
|
||||||
|
usleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EOF
|
202
RpiLedBars/src/rpi_dma_utils.h
Normal file
202
RpiLedBars/src/rpi_dma_utils.h
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
// Raspberry Pi DMA utility definitions; see https://iosoft.blog for details
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020 Jeremy P Bentham
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Location of peripheral registers in physical memory
|
||||||
|
#define PHYS_REG_BASE PI_01_REG_BASE
|
||||||
|
#define PI_01_REG_BASE 0x20000000 // Pi Zero or 1
|
||||||
|
#define PI_23_REG_BASE 0x3F000000 // Pi 2 or 3
|
||||||
|
#define PI_4_REG_BASE 0xFE000000 // Pi 4
|
||||||
|
|
||||||
|
#define CLOCK_HZ 250000000 // Pi 2 - 4
|
||||||
|
//#define CLOCK_HZ 400000000 // Pi Zero
|
||||||
|
|
||||||
|
// Location of peripheral registers in bus memory
|
||||||
|
#define BUS_REG_BASE 0x7E000000
|
||||||
|
|
||||||
|
// If non-zero, print debug information
|
||||||
|
#define DEBUG 0
|
||||||
|
|
||||||
|
// If non-zero, set PWM clock using VideoCore mailbox
|
||||||
|
#define USE_VC_CLOCK_SET 0
|
||||||
|
|
||||||
|
// Size of memory page
|
||||||
|
#define PAGE_SIZE 0x1000
|
||||||
|
// Round up to nearest page
|
||||||
|
#define PAGE_ROUNDUP(n) ((n)%PAGE_SIZE==0 ? (n) : ((n)+PAGE_SIZE)&~(PAGE_SIZE-1))
|
||||||
|
|
||||||
|
// Structure for mapped peripheral or memory
|
||||||
|
typedef struct {
|
||||||
|
int fd, // File descriptor
|
||||||
|
h, // Memory handle
|
||||||
|
size; // Memory size
|
||||||
|
void *bus, // Bus address
|
||||||
|
*virt, // Virtual address
|
||||||
|
*phys; // Physical address
|
||||||
|
} MEM_MAP;
|
||||||
|
|
||||||
|
// Get virtual 8 and 32-bit pointers to register
|
||||||
|
#define REG8(m, x) ((volatile uint8_t *) ((uint32_t)(m.virt)+(uint32_t)(x)))
|
||||||
|
#define REG32(m, x) ((volatile uint32_t *)((uint32_t)(m.virt)+(uint32_t)(x)))
|
||||||
|
// Get bus address of register
|
||||||
|
#define REG_BUS_ADDR(m, x) ((uint32_t)(m.bus) + (uint32_t)(x))
|
||||||
|
// Convert uncached memory virtual address to bus address
|
||||||
|
#define MEM_BUS_ADDR(mp, a) ((uint32_t)a-(uint32_t)mp->virt+(uint32_t)mp->bus)
|
||||||
|
// Convert bus address to physical address (for mmap)
|
||||||
|
#define BUS_PHYS_ADDR(a) ((void *)((uint32_t)(a)&~0xC0000000))
|
||||||
|
|
||||||
|
// GPIO register definitions
|
||||||
|
#define GPIO_BASE (PHYS_REG_BASE + 0x200000)
|
||||||
|
#define GPIO_MODE0 0x00
|
||||||
|
#define GPIO_SET0 0x1c
|
||||||
|
#define GPIO_CLR0 0x28
|
||||||
|
#define GPIO_LEV0 0x34
|
||||||
|
#define GPIO_GPPUD 0x94
|
||||||
|
#define GPIO_GPPUDCLK0 0x98
|
||||||
|
// GPIO I/O definitions
|
||||||
|
#define GPIO_IN 0
|
||||||
|
#define GPIO_OUT 1
|
||||||
|
#define GPIO_ALT0 4
|
||||||
|
#define GPIO_ALT1 5
|
||||||
|
#define GPIO_ALT2 6
|
||||||
|
#define GPIO_ALT3 7
|
||||||
|
#define GPIO_ALT4 3
|
||||||
|
#define GPIO_ALT5 2
|
||||||
|
#define GPIO_MODE_STRS "IN","OUT","ALT5","ALT4","ALT0","ALT1","ALT2","ALT3"
|
||||||
|
#define GPIO_NOPULL 0
|
||||||
|
#define GPIO_PULLDN 1
|
||||||
|
#define GPIO_PULLUP 2
|
||||||
|
|
||||||
|
// Videocore mailbox memory allocation flags, see:
|
||||||
|
// https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface
|
||||||
|
typedef enum {
|
||||||
|
MEM_FLAG_DISCARDABLE = 1<<0, // can be resized to 0 at any time. Use for cached data
|
||||||
|
MEM_FLAG_NORMAL = 0<<2, // normal allocating alias. Don't use from ARM
|
||||||
|
MEM_FLAG_DIRECT = 1<<2, // 0xC alias uncached
|
||||||
|
MEM_FLAG_COHERENT = 2<<2, // 0x8 alias. Non-allocating in L2 but coherent
|
||||||
|
MEM_FLAG_ZERO = 1<<4, // initialise buffer to all zeros
|
||||||
|
MEM_FLAG_NO_INIT = 1<<5, // don't initialise (default is initialise to all ones)
|
||||||
|
MEM_FLAG_HINT_PERMALOCK = 1<<6, // Likely to be locked for long periods of time
|
||||||
|
MEM_FLAG_L1_NONALLOCATING=(MEM_FLAG_DIRECT | MEM_FLAG_COHERENT) // Allocating in L2
|
||||||
|
} VC_ALLOC_FLAGS;
|
||||||
|
// VC flags for unchached DMA memory
|
||||||
|
#define DMA_MEM_FLAGS (MEM_FLAG_DIRECT|MEM_FLAG_ZERO)
|
||||||
|
|
||||||
|
// Mailbox command/response structure
|
||||||
|
typedef struct {
|
||||||
|
uint32_t len, // Overall length (bytes)
|
||||||
|
req, // Zero for request, 1<<31 for response
|
||||||
|
tag, // Command number
|
||||||
|
blen, // Buffer length (bytes)
|
||||||
|
dlen; // Data length (bytes)
|
||||||
|
uint32_t uints[32-5]; // Data (108 bytes maximum)
|
||||||
|
} VC_MSG __attribute__ ((aligned (16)));
|
||||||
|
|
||||||
|
// DMA channels and data requests
|
||||||
|
#define DMA_CHAN_A 10
|
||||||
|
#define DMA_CHAN_B 11
|
||||||
|
#define DMA_PWM_DREQ 5
|
||||||
|
#define DMA_SPI_TX_DREQ 6
|
||||||
|
#define DMA_SPI_RX_DREQ 7
|
||||||
|
#define DMA_BASE (PHYS_REG_BASE + 0x007000)
|
||||||
|
// DMA register addresses offset by 0x100 * chan_num
|
||||||
|
#define DMA_CS 0x00
|
||||||
|
#define DMA_CONBLK_AD 0x04
|
||||||
|
#define DMA_TI 0x08
|
||||||
|
#define DMA_SRCE_AD 0x0c
|
||||||
|
#define DMA_DEST_AD 0x10
|
||||||
|
#define DMA_TXFR_LEN 0x14
|
||||||
|
#define DMA_STRIDE 0x18
|
||||||
|
#define DMA_NEXTCONBK 0x1c
|
||||||
|
#define DMA_DEBUG 0x20
|
||||||
|
#define DMA_REG(ch, r) ((r)==DMA_ENABLE ? DMA_ENABLE : (ch)*0x100+(r))
|
||||||
|
#define DMA_ENABLE 0xff0
|
||||||
|
// DMA register values
|
||||||
|
#define DMA_WAIT_RESP (1 << 3)
|
||||||
|
#define DMA_CB_DEST_INC (1 << 4)
|
||||||
|
#define DMA_DEST_DREQ (1 << 6)
|
||||||
|
#define DMA_CB_SRCE_INC (1 << 8)
|
||||||
|
#define DMA_SRCE_DREQ (1 << 10)
|
||||||
|
#define DMA_PRIORITY(n) ((n) << 16)
|
||||||
|
|
||||||
|
// DMA control block (must be 32-byte aligned)
|
||||||
|
typedef struct {
|
||||||
|
uint32_t ti, // Transfer info
|
||||||
|
srce_ad, // Source address
|
||||||
|
dest_ad, // Destination address
|
||||||
|
tfr_len, // Transfer length
|
||||||
|
stride, // Transfer stride
|
||||||
|
next_cb, // Next control block
|
||||||
|
debug, // Debug register, zero in control block
|
||||||
|
unused;
|
||||||
|
} DMA_CB __attribute__ ((aligned(32)));
|
||||||
|
|
||||||
|
// PWM controller registers
|
||||||
|
#define PWM_BASE (PHYS_REG_BASE + 0x20C000)
|
||||||
|
#define PWM_CTL 0x00 // Control
|
||||||
|
#define PWM_STA 0x04 // Status
|
||||||
|
#define PWM_DMAC 0x08 // DMA control
|
||||||
|
#define PWM_RNG1 0x10 // Channel 1 range
|
||||||
|
#define PWM_DAT1 0x14 // Channel 1 data
|
||||||
|
#define PWM_FIF1 0x18 // Channel 1 fifo
|
||||||
|
#define PWM_RNG2 0x20 // Channel 2 range
|
||||||
|
#define PWM_DAT2 0x24 // Channel 2 data
|
||||||
|
// PWM register values
|
||||||
|
#define PWM_CTL_RPTL1 (1<<2) // Chan 1: repeat last data when FIFO empty
|
||||||
|
#define PWM_CTL_USEF1 (1<<5) // Chan 1: use FIFO
|
||||||
|
#define PWM_DMAC_ENAB (1<<31) // Start PWM DMA
|
||||||
|
#define PWM_ENAB 1 // Enable PWM
|
||||||
|
#define PWM_PIN 12 // GPIO pin for PWM output, 12 or 18
|
||||||
|
|
||||||
|
// Clock registers and values
|
||||||
|
#define CLK_BASE (PHYS_REG_BASE + 0x101000)
|
||||||
|
#define CLK_PWM_CTL 0xa0
|
||||||
|
#define CLK_PWM_DIV 0xa4
|
||||||
|
#define CLK_PASSWD 0x5a000000
|
||||||
|
#define PWM_CLOCK_ID 0xa
|
||||||
|
|
||||||
|
void fail(char *s);
|
||||||
|
void *map_periph(MEM_MAP *mp, void *phys, int size);
|
||||||
|
void *map_uncached_mem(MEM_MAP *mp, int size);
|
||||||
|
void unmap_periph_mem(MEM_MAP *mp);
|
||||||
|
void gpio_set(int pin, int mode, int pull);
|
||||||
|
void gpio_pull(int pin, int pull);
|
||||||
|
void gpio_mode(int pin, int mode);
|
||||||
|
void gpio_out(int pin, int val);
|
||||||
|
uint8_t gpio_in(int pin);
|
||||||
|
void disp_mode_vals(uint32_t mode);
|
||||||
|
int open_mbox(void);
|
||||||
|
void close_mbox(int fd);
|
||||||
|
uint32_t msg_mbox(int fd, VC_MSG *msgp);
|
||||||
|
void *map_segment(void *addr, int size);
|
||||||
|
void unmap_segment(void *addr, int size);
|
||||||
|
uint32_t alloc_vc_mem(int fd, uint32_t size, VC_ALLOC_FLAGS flags);
|
||||||
|
void *lock_vc_mem(int fd, int h);
|
||||||
|
uint32_t unlock_vc_mem(int fd, int h);
|
||||||
|
uint32_t free_vc_mem(int fd, int h);
|
||||||
|
uint32_t set_vc_clock(int fd, int id, uint32_t freq);
|
||||||
|
void disp_vc_msg(VC_MSG *msgp);
|
||||||
|
void enable_dma(int chan);
|
||||||
|
void start_dma(MEM_MAP *mp, int chan, DMA_CB *cbp, uint32_t csval);
|
||||||
|
uint32_t dma_transfer_len(int chan);
|
||||||
|
uint32_t dma_active(int chan);
|
||||||
|
void stop_dma(int chan);
|
||||||
|
void disp_dma(int chan);
|
||||||
|
void init_pwm(int freq, int range, int val);
|
||||||
|
void start_pwm(void);
|
||||||
|
void stop_pwm(void);
|
||||||
|
|
||||||
|
// EOF
|
389
RpiLedBars/src/rpi_pixleds.c
Normal file
389
RpiLedBars/src/rpi_pixleds.c
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
// Raspberry Pi WS2812 LED driver using SMI
|
||||||
|
// For detailed description, see https://iosoft.blog
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020 Jeremy P Bentham
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// v0.01 JPB 16/7/20 Adapted from rpi_smi_adc_test v0.06
|
||||||
|
// v0.02 JPB 15/9/20 Addded RGB to GRB conversion
|
||||||
|
// v0.03 JPB 15/9/20 Added red-green flashing
|
||||||
|
// v0.04 JPB 16/9/20 Added test mode
|
||||||
|
// v0.05 JPB 19/9/20 Changed test mode colours
|
||||||
|
// v0.06 JPB 20/9/20 Outlined command-line data input
|
||||||
|
// v0.07 JPB 25/9/20 Command-line data input if not in test mode
|
||||||
|
// v0.08 JPB 26/9/20 Changed from 4 to 3 pulses per LED bit
|
||||||
|
// Added 4-bit zero preamble
|
||||||
|
// Added raw Tx data test
|
||||||
|
// v0.09 JPB 27/9/20 Added 16-channel option
|
||||||
|
// v0.10 JPB 28/9/20 Corrected Pi Zero caching problem
|
||||||
|
// v0.11 JPB 29/9/20 Added enable_dma before transfer (in case still active)
|
||||||
|
// Corrected DMA nsamp value (was byte count)
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include "rpi_dma_utils.h"
|
||||||
|
#include "rpi_smi_defs.h"
|
||||||
|
|
||||||
|
#if PHYS_REG_BASE==PI_4_REG_BASE // Timings for RPi v4 (1.5 GHz)
|
||||||
|
#define SMI_TIMING 10, 15, 30, 15 // 400 ns cycle time
|
||||||
|
#else // Timings for RPi v0-3 (1 GHz)
|
||||||
|
#define SMI_TIMING 10, 10, 20, 10 // 400 ns cycle time
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define TX_TEST 0 // If non-zero, use dummy Tx data
|
||||||
|
#define LED_D0_PIN 8 // GPIO pin for D0 output
|
||||||
|
#define LED_NCHANS 8 // Number of LED channels (8 or 16)
|
||||||
|
#define LED_NBITS 24 // Number of data bits per LED
|
||||||
|
#define LED_PREBITS 4 // Number of zero bits before LED data
|
||||||
|
#define LED_POSTBITS 4 // Number of zero bits after LED data
|
||||||
|
#define BIT_NPULSES 3 // Number of O/P pulses per LED bit
|
||||||
|
#define CHAN_MAXLEDS 50 // Maximum number of LEDs per channel
|
||||||
|
#define CHASE_MSEC 100 // Delay time for chaser light test
|
||||||
|
#define REQUEST_THRESH 2 // DMA request threshold
|
||||||
|
#define DMA_CHAN 10 // DMA channel to use
|
||||||
|
|
||||||
|
// Length of data for 1 row (1 LED on each channel)
|
||||||
|
#define LED_DLEN (LED_NBITS * BIT_NPULSES)
|
||||||
|
|
||||||
|
// Transmit data type, 8 or 16 bits
|
||||||
|
#if LED_NCHANS > 8
|
||||||
|
#define TXDATA_T uint16_t
|
||||||
|
#else
|
||||||
|
#define TXDATA_T uint8_t
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Structures for mapped I/O devices, and non-volatile memory
|
||||||
|
extern MEM_MAP gpio_regs, dma_regs;
|
||||||
|
MEM_MAP vc_mem, clk_regs, smi_regs;
|
||||||
|
|
||||||
|
// Pointers to SMI registers
|
||||||
|
volatile SMI_CS_REG *smi_cs;
|
||||||
|
volatile SMI_L_REG *smi_l;
|
||||||
|
volatile SMI_A_REG *smi_a;
|
||||||
|
volatile SMI_D_REG *smi_d;
|
||||||
|
volatile SMI_DMC_REG *smi_dmc;
|
||||||
|
volatile SMI_DSR_REG *smi_dsr;
|
||||||
|
volatile SMI_DSW_REG *smi_dsw;
|
||||||
|
volatile SMI_DCS_REG *smi_dcs;
|
||||||
|
volatile SMI_DCA_REG *smi_dca;
|
||||||
|
volatile SMI_DCD_REG *smi_dcd;
|
||||||
|
|
||||||
|
// Ofset into Tx data buffer, given LED number in chan
|
||||||
|
#define LED_TX_OSET(n) (LED_PREBITS + (LED_DLEN * (n)))
|
||||||
|
|
||||||
|
// Size of data buffers & NV memory, given number of LEDs per chan
|
||||||
|
#define TX_BUFF_LEN(n) (LED_TX_OSET(n) + LED_POSTBITS)
|
||||||
|
#define TX_BUFF_SIZE(n) (TX_BUFF_LEN(n) * sizeof(TXDATA_T))
|
||||||
|
#define VC_MEM_SIZE (PAGE_SIZE + TX_BUFF_SIZE(CHAN_MAXLEDS))
|
||||||
|
|
||||||
|
// RGB values for test mode (1 value for each of 16 channels)
|
||||||
|
int on_rgbs[16] = {0xff0000, 0x00ff00, 0x0000ff, 0xffffff,
|
||||||
|
0xff4040, 0x40ff40, 0x4040ff, 0x404040,
|
||||||
|
0xff0000, 0x00ff00, 0x0000ff, 0xffffff,
|
||||||
|
0xff4040, 0x40ff40, 0x4040ff, 0x404040};
|
||||||
|
int off_rgbs[16];
|
||||||
|
|
||||||
|
#if TX_TEST
|
||||||
|
// Data for simple transmission test
|
||||||
|
TXDATA_T tx_test_data[] = {1, 2, 3, 4, 5, 6, 7, 0};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TXDATA_T *txdata; // Pointer to uncached Tx data buffer
|
||||||
|
TXDATA_T tx_buffer[TX_BUFF_LEN(CHAN_MAXLEDS)]; // Tx buffer for assembling data
|
||||||
|
int testmode, chan_ledcount=1; // Command-line parameters
|
||||||
|
int rgb_data[CHAN_MAXLEDS][LED_NCHANS]; // RGB data
|
||||||
|
int chan_num; // Current channel for data I/P
|
||||||
|
|
||||||
|
void rgb_txdata(int *rgbs, TXDATA_T *txd);
|
||||||
|
int str_rgb(char *s, int rgbs[][LED_NCHANS], int chan);
|
||||||
|
void swap_bytes(void *data, int len);
|
||||||
|
int hexdig(char c);
|
||||||
|
void map_devices(void);
|
||||||
|
void fail(char *s);
|
||||||
|
void terminate(int sig);
|
||||||
|
void init_smi(int width, int ns, int setup, int hold, int strobe);
|
||||||
|
void setup_smi_dma(MEM_MAP *mp, int nsamp);
|
||||||
|
void start_smi(MEM_MAP *mp);
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
int args=0, n, oset=0;
|
||||||
|
|
||||||
|
while (argc > ++args) // Process command-line args
|
||||||
|
{
|
||||||
|
if (argv[args][0] == '-')
|
||||||
|
{
|
||||||
|
switch (toupper(argv[args][1]))
|
||||||
|
{
|
||||||
|
case 'N': // -N: number of LEDs per channel
|
||||||
|
if (args >= argc-1)
|
||||||
|
fprintf(stderr, "Error: no numeric value\n");
|
||||||
|
else
|
||||||
|
chan_ledcount = atoi(argv[++args]);
|
||||||
|
break;
|
||||||
|
case 'T': // -T: test mode
|
||||||
|
testmode = 1;
|
||||||
|
break;
|
||||||
|
default: // Otherwise error
|
||||||
|
printf("Unrecognised option '%c'\n", argv[args][1]);
|
||||||
|
printf("Options:\n"
|
||||||
|
" -n num number of LEDs per channel\n"\
|
||||||
|
" -t Test mode (flash LEDs)\n"\
|
||||||
|
);
|
||||||
|
return(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (chan_num<LED_NCHANS && hexdig(argv[args][0])>=0 &&
|
||||||
|
(n=str_rgb(argv[args], rgb_data, chan_num))>0)
|
||||||
|
{
|
||||||
|
chan_ledcount = n > chan_ledcount ? n : chan_ledcount;
|
||||||
|
chan_num++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signal(SIGINT, terminate);
|
||||||
|
map_devices();
|
||||||
|
init_smi(LED_NCHANS>8 ? SMI_16_BITS : SMI_8_BITS, SMI_TIMING);
|
||||||
|
map_uncached_mem(&vc_mem, VC_MEM_SIZE);
|
||||||
|
#if TX_TEST
|
||||||
|
oset = oset;
|
||||||
|
setup_smi_dma(&vc_mem, sizeof(tx_test_data)/sizeof(TXDATA_T));
|
||||||
|
#if LED_NCHANS <= 8
|
||||||
|
swap_bytes(tx_test_data, sizeof(tx_test_data));
|
||||||
|
#endif
|
||||||
|
memcpy(txdata, tx_test_data, sizeof(tx_test_data));
|
||||||
|
start_smi(&vc_mem);
|
||||||
|
usleep(10);
|
||||||
|
while (dma_active(DMA_CHAN))
|
||||||
|
usleep(10);
|
||||||
|
#else
|
||||||
|
setup_smi_dma(&vc_mem, TX_BUFF_LEN(chan_ledcount));
|
||||||
|
printf("%s %u LED%s per channel, %u channels\n", testmode ? "Testing" : "Setting",
|
||||||
|
chan_ledcount, chan_ledcount==1 ? "" : "s", LED_NCHANS);
|
||||||
|
|
||||||
|
if (testmode)
|
||||||
|
{
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
if (chan_ledcount < 2)
|
||||||
|
rgb_txdata(oset&1 ? off_rgbs : on_rgbs, tx_buffer);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (n=0; n<chan_ledcount; n++)
|
||||||
|
{
|
||||||
|
rgb_txdata(n==oset%chan_ledcount ? on_rgbs : off_rgbs,
|
||||||
|
&tx_buffer[LED_TX_OSET(n)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oset++;
|
||||||
|
#if LED_NCHANS <= 8
|
||||||
|
swap_bytes(tx_buffer, TX_BUFF_SIZE(chan_ledcount));
|
||||||
|
#endif
|
||||||
|
memcpy(txdata, tx_buffer, TX_BUFF_SIZE(chan_ledcount));
|
||||||
|
start_smi(&vc_mem);
|
||||||
|
usleep(CHASE_MSEC * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (n=0; n<chan_ledcount; n++)
|
||||||
|
rgb_txdata(rgb_data[n], &tx_buffer[LED_TX_OSET(n)]);
|
||||||
|
#if LED_NCHANS <= 8
|
||||||
|
swap_bytes(tx_buffer, TX_BUFF_SIZE(chan_ledcount));
|
||||||
|
#endif
|
||||||
|
memcpy(txdata, tx_buffer, TX_BUFF_SIZE(chan_ledcount));
|
||||||
|
enable_dma(DMA_CHAN);
|
||||||
|
start_smi(&vc_mem);
|
||||||
|
usleep(10);
|
||||||
|
while (dma_active(DMA_CHAN))
|
||||||
|
usleep(10);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
terminate(0);
|
||||||
|
return(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert RGB text string into integer data, for given channel
|
||||||
|
// Return number of data points for this channel
|
||||||
|
int str_rgb(char *s, int rgbs[][LED_NCHANS], int chan)
|
||||||
|
{
|
||||||
|
int i=0;
|
||||||
|
char *p;
|
||||||
|
|
||||||
|
while (chan<LED_NCHANS && i<CHAN_MAXLEDS && hexdig(*s)>=0)
|
||||||
|
{
|
||||||
|
rgbs[i++][chan] = strtoul(s, &p, 16);
|
||||||
|
s = *p ? p+1 : p;
|
||||||
|
}
|
||||||
|
return(i);
|
||||||
|
}
|
||||||
|
// Set Tx data for 8 or 16 chans, 1 LED per chan, given 1 RGB val per chan
|
||||||
|
// Logic 1 is 0.8us high, 0.4 us low, logic 0 is 0.4us high, 0.8us low
|
||||||
|
void rgb_txdata(int *rgbs, TXDATA_T *txd)
|
||||||
|
{
|
||||||
|
int i, n, msk;
|
||||||
|
|
||||||
|
// For each bit of the 24-bit RGB values..
|
||||||
|
for (n=0; n<LED_NBITS; n++)
|
||||||
|
{
|
||||||
|
// Mask to convert RGB to GRB, M.S bit first
|
||||||
|
msk = n==0 ? 0x8000 : n==8 ? 0x800000 : n==16 ? 0x80 : msk>>1;
|
||||||
|
// 1st byte or word is a high pulse on all lines
|
||||||
|
txd[0] = (TXDATA_T)0xffff;
|
||||||
|
// 2nd has high or low bits from data
|
||||||
|
// 3rd is a low pulse
|
||||||
|
txd[1] = txd[2] = 0;
|
||||||
|
for (i=0; i<LED_NCHANS; i++)
|
||||||
|
{
|
||||||
|
if (rgbs[i] & msk)
|
||||||
|
txd[1] |= (1 << i);
|
||||||
|
}
|
||||||
|
txd += BIT_NPULSES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap adjacent bytes in transmit data
|
||||||
|
void swap_bytes(void *data, int len)
|
||||||
|
{
|
||||||
|
uint16_t *wp = (uint16_t *)data;
|
||||||
|
|
||||||
|
len = (len + 1) / 2;
|
||||||
|
while (len-- > 0)
|
||||||
|
{
|
||||||
|
*wp = __builtin_bswap16(*wp);
|
||||||
|
wp++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return hex digit value, -ve if not hex
|
||||||
|
int hexdig(char c)
|
||||||
|
{
|
||||||
|
c = toupper(c);
|
||||||
|
return((c>='0' && c<='9') ? c-'0' : (c>='A' && c<='F') ? c-'A'+10 : -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map GPIO, DMA and SMI registers into virtual mem (user space)
|
||||||
|
// If any of these fail, program will be terminated
|
||||||
|
void map_devices(void)
|
||||||
|
{
|
||||||
|
map_periph(&gpio_regs, (void *)GPIO_BASE, PAGE_SIZE);
|
||||||
|
map_periph(&dma_regs, (void *)DMA_BASE, PAGE_SIZE);
|
||||||
|
map_periph(&clk_regs, (void *)CLK_BASE, PAGE_SIZE);
|
||||||
|
map_periph(&smi_regs, (void *)SMI_BASE, PAGE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catastrophic failure in initial setup
|
||||||
|
void fail(char *s)
|
||||||
|
{
|
||||||
|
printf(s);
|
||||||
|
terminate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free memory segments and exit
|
||||||
|
void terminate(int sig)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
printf("Closing\n");
|
||||||
|
if (gpio_regs.virt)
|
||||||
|
{
|
||||||
|
for (i=0; i<LED_NCHANS; i++)
|
||||||
|
gpio_mode(LED_D0_PIN+i, GPIO_IN);
|
||||||
|
}
|
||||||
|
if (smi_regs.virt)
|
||||||
|
*REG32(smi_regs, SMI_CS) = 0;
|
||||||
|
stop_dma(DMA_CHAN);
|
||||||
|
unmap_periph_mem(&vc_mem);
|
||||||
|
unmap_periph_mem(&smi_regs);
|
||||||
|
unmap_periph_mem(&dma_regs);
|
||||||
|
unmap_periph_mem(&gpio_regs);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise SMI, given data width, time step, and setup/hold/strobe counts
|
||||||
|
// Step value is in nanoseconds: even numbers, 2 to 30
|
||||||
|
void init_smi(int width, int ns, int setup, int strobe, int hold)
|
||||||
|
{
|
||||||
|
int i, divi = ns / 2;
|
||||||
|
|
||||||
|
smi_cs = (SMI_CS_REG *) REG32(smi_regs, SMI_CS);
|
||||||
|
smi_l = (SMI_L_REG *) REG32(smi_regs, SMI_L);
|
||||||
|
smi_a = (SMI_A_REG *) REG32(smi_regs, SMI_A);
|
||||||
|
smi_d = (SMI_D_REG *) REG32(smi_regs, SMI_D);
|
||||||
|
smi_dmc = (SMI_DMC_REG *)REG32(smi_regs, SMI_DMC);
|
||||||
|
smi_dsr = (SMI_DSR_REG *)REG32(smi_regs, SMI_DSR0);
|
||||||
|
smi_dsw = (SMI_DSW_REG *)REG32(smi_regs, SMI_DSW0);
|
||||||
|
smi_dcs = (SMI_DCS_REG *)REG32(smi_regs, SMI_DCS);
|
||||||
|
smi_dca = (SMI_DCA_REG *)REG32(smi_regs, SMI_DCA);
|
||||||
|
smi_dcd = (SMI_DCD_REG *)REG32(smi_regs, SMI_DCD);
|
||||||
|
smi_cs->value = smi_l->value = smi_a->value = 0;
|
||||||
|
smi_dsr->value = smi_dsw->value = smi_dcs->value = smi_dca->value = 0;
|
||||||
|
if (*REG32(clk_regs, CLK_SMI_DIV) != divi << 12)
|
||||||
|
{
|
||||||
|
*REG32(clk_regs, CLK_SMI_CTL) = CLK_PASSWD | (1 << 5);
|
||||||
|
usleep(10);
|
||||||
|
while (*REG32(clk_regs, CLK_SMI_CTL) & (1 << 7)) ;
|
||||||
|
usleep(10);
|
||||||
|
*REG32(clk_regs, CLK_SMI_DIV) = CLK_PASSWD | (divi << 12);
|
||||||
|
usleep(10);
|
||||||
|
*REG32(clk_regs, CLK_SMI_CTL) = CLK_PASSWD | 6 | (1 << 4);
|
||||||
|
usleep(10);
|
||||||
|
while ((*REG32(clk_regs, CLK_SMI_CTL) & (1 << 7)) == 0) ;
|
||||||
|
usleep(100);
|
||||||
|
}
|
||||||
|
if (smi_cs->seterr)
|
||||||
|
smi_cs->seterr = 1;
|
||||||
|
smi_dsr->rsetup = smi_dsw->wsetup = setup;
|
||||||
|
smi_dsr->rstrobe = smi_dsw->wstrobe = strobe;
|
||||||
|
smi_dsr->rhold = smi_dsw->whold = hold;
|
||||||
|
smi_dmc->panicr = smi_dmc->panicw = 8;
|
||||||
|
smi_dmc->reqr = smi_dmc->reqw = REQUEST_THRESH;
|
||||||
|
smi_dsr->rwidth = smi_dsw->wwidth = width;
|
||||||
|
for (i=0; i<LED_NCHANS; i++)
|
||||||
|
gpio_mode(LED_D0_PIN+i, GPIO_ALT1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up SMI transfers using DMA
|
||||||
|
void setup_smi_dma(MEM_MAP *mp, int nsamp)
|
||||||
|
{
|
||||||
|
DMA_CB *cbs=mp->virt;
|
||||||
|
|
||||||
|
txdata = (TXDATA_T *)(cbs+1);
|
||||||
|
smi_dmc->dmaen = 1;
|
||||||
|
smi_cs->enable = 1;
|
||||||
|
smi_cs->clear = 1;
|
||||||
|
smi_cs->pxldat = 1;
|
||||||
|
smi_l->len = nsamp * sizeof(TXDATA_T);
|
||||||
|
smi_cs->write = 1;
|
||||||
|
enable_dma(DMA_CHAN);
|
||||||
|
cbs[0].ti = DMA_DEST_DREQ | (DMA_SMI_DREQ << 16) | DMA_CB_SRCE_INC | DMA_WAIT_RESP;
|
||||||
|
cbs[0].tfr_len = nsamp;
|
||||||
|
cbs[0].srce_ad = MEM_BUS_ADDR(mp, txdata);
|
||||||
|
cbs[0].dest_ad = REG_BUS_ADDR(smi_regs, SMI_D);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start SMI DMA transfers
|
||||||
|
void start_smi(MEM_MAP *mp)
|
||||||
|
{
|
||||||
|
DMA_CB *cbs=mp->virt;
|
||||||
|
|
||||||
|
start_dma(mp, DMA_CHAN, &cbs[0], 0);
|
||||||
|
smi_cs->start = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EOF
|
100
RpiLedBars/src/rpi_smi_defs.h
Normal file
100
RpiLedBars/src/rpi_smi_defs.h
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// Secondary Memory Interface definitions for Raspberry Pi
|
||||||
|
//
|
||||||
|
// v0.01 JPB 12/7/20 Adapted from rpi_smi_test v0.19
|
||||||
|
|
||||||
|
// Register definitions
|
||||||
|
#define SMI_BASE (PHYS_REG_BASE + 0x600000)
|
||||||
|
#define SMI_CS 0x00 // Control & status
|
||||||
|
#define SMI_L 0x04 // Transfer length
|
||||||
|
#define SMI_A 0x08 // Address
|
||||||
|
#define SMI_D 0x0c // Data
|
||||||
|
#define SMI_DSR0 0x10 // Read settings device 0
|
||||||
|
#define SMI_DSW0 0x14 // Write settings device 0
|
||||||
|
#define SMI_DSR1 0x18 // Read settings device 1
|
||||||
|
#define SMI_DSW1 0x1c // Write settings device 1
|
||||||
|
#define SMI_DSR2 0x20 // Read settings device 2
|
||||||
|
#define SMI_DSW2 0x24 // Write settings device 2
|
||||||
|
#define SMI_DSR3 0x28 // Read settings device 3
|
||||||
|
#define SMI_DSW3 0x2c // Write settings device 3
|
||||||
|
#define SMI_DMC 0x30 // DMA control
|
||||||
|
#define SMI_DCS 0x34 // Direct control/status
|
||||||
|
#define SMI_DCA 0x38 // Direct address
|
||||||
|
#define SMI_DCD 0x3c // Direct data
|
||||||
|
#define SMI_FD 0x40 // FIFO debug
|
||||||
|
#define SMI_REGLEN (SMI_FD * 4)
|
||||||
|
|
||||||
|
// Data widths
|
||||||
|
#define SMI_8_BITS 0
|
||||||
|
#define SMI_16_BITS 1
|
||||||
|
#define SMI_18_BITS 2
|
||||||
|
#define SMI_9_BITS 3
|
||||||
|
|
||||||
|
// DMA request
|
||||||
|
#define DMA_SMI_DREQ 4
|
||||||
|
|
||||||
|
// Union of 32-bit value with register bitfields
|
||||||
|
#define REG_DEF(name, fields) typedef union {struct {volatile uint32_t fields;}; volatile uint32_t value;} name
|
||||||
|
|
||||||
|
// Control and status register
|
||||||
|
#define SMI_CS_FIELDS \
|
||||||
|
enable:1, done:1, active:1, start:1, clear:1, write:1, _x1:2,\
|
||||||
|
teen:1, intd:1, intt:1, intr:1, pvmode:1, seterr:1, pxldat:1, edreq:1,\
|
||||||
|
_x2:8, _x3:1, aferr:1, txw:1, rxr:1, txd:1, rxd:1, txe:1, rxf:1
|
||||||
|
REG_DEF(SMI_CS_REG, SMI_CS_FIELDS);
|
||||||
|
|
||||||
|
// Data length register
|
||||||
|
#define SMI_L_FIELDS \
|
||||||
|
len:32
|
||||||
|
REG_DEF(SMI_L_REG, SMI_L_FIELDS);
|
||||||
|
|
||||||
|
// Address & device number
|
||||||
|
#define SMI_A_FIELDS \
|
||||||
|
addr:6, _x1:2, dev:2
|
||||||
|
REG_DEF(SMI_A_REG, SMI_A_FIELDS);
|
||||||
|
|
||||||
|
// Data FIFO
|
||||||
|
#define SMI_D_FIELDS \
|
||||||
|
data:32
|
||||||
|
REG_DEF(SMI_D_REG, SMI_D_FIELDS);
|
||||||
|
|
||||||
|
// DMA control register
|
||||||
|
#define SMI_DMC_FIELDS \
|
||||||
|
reqw:6, reqr:6, panicw:6, panicr:6, dmap:1, _x1:3, dmaen:1
|
||||||
|
REG_DEF(SMI_DMC_REG, SMI_DMC_FIELDS);
|
||||||
|
|
||||||
|
// Device settings: read (1 of 4)
|
||||||
|
#define SMI_DSR_FIELDS \
|
||||||
|
rstrobe:7, rdreq:1, rpace:7, rpaceall:1, \
|
||||||
|
rhold:6, fsetup:1, mode68:1, rsetup:6, rwidth:2
|
||||||
|
REG_DEF(SMI_DSR_REG, SMI_DSR_FIELDS);
|
||||||
|
|
||||||
|
// Device settings: write (1 of 4)
|
||||||
|
#define SMI_DSW_FIELDS \
|
||||||
|
wstrobe:7, wdreq:1, wpace:7, wpaceall:1, \
|
||||||
|
whold:6, wswap:1, wformat:1, wsetup:6, wwidth:2
|
||||||
|
REG_DEF(SMI_DSW_REG, SMI_DSW_FIELDS);
|
||||||
|
|
||||||
|
// Direct control register
|
||||||
|
#define SMI_DCS_FIELDS \
|
||||||
|
enable:1, start:1, done:1, write:1
|
||||||
|
REG_DEF(SMI_DCS_REG, SMI_DCS_FIELDS);
|
||||||
|
|
||||||
|
// Direct control address & device number
|
||||||
|
#define SMI_DCA_FIELDS \
|
||||||
|
addr:6, _x1:2, dev:2
|
||||||
|
REG_DEF(SMI_DCA_REG, SMI_DCA_FIELDS);
|
||||||
|
|
||||||
|
// Direct control data
|
||||||
|
#define SMI_DCD_FIELDS \
|
||||||
|
data:32
|
||||||
|
REG_DEF(SMI_DCD_REG, SMI_DCD_FIELDS);
|
||||||
|
|
||||||
|
// Debug register
|
||||||
|
#define SMI_FLVL_FIELDS \
|
||||||
|
fcnt:6, _x1:2, flvl:6
|
||||||
|
REG_DEF(SMI_FLVL_REG, SMI_FLVL_FIELDS);
|
||||||
|
|
||||||
|
#define CLK_SMI_CTL 0xb0
|
||||||
|
#define CLK_SMI_DIV 0xb4
|
||||||
|
|
||||||
|
// EOF
|
176
src/main.cpp
176
src/main.cpp
@ -1,176 +0,0 @@
|
|||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
// TODO : use core built-in lib tinyNeoPixel
|
|
||||||
#include <Adafruit_NeoPixel.h>
|
|
||||||
#include <Encoder.h>
|
|
||||||
|
|
||||||
#include "pin_map.hpp"
|
|
||||||
|
|
||||||
/* Types and Constants */
|
|
||||||
/*------------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
/* State machine */
|
|
||||||
#define FOREACH_MODE(MODE) \
|
|
||||||
MODE(Idle) \
|
|
||||||
MODE(Chaser) \
|
|
||||||
MODE(Rainbow)
|
|
||||||
|
|
||||||
#define GENERATE_MODE_ENUM(ENUM) ENUM,
|
|
||||||
|
|
||||||
enum class Mode_e { FOREACH_MODE(GENERATE_MODE_ENUM) };
|
|
||||||
|
|
||||||
Mode_e &operator++(Mode_e &mode) {
|
|
||||||
if (mode == Mode_e::Rainbow) {
|
|
||||||
return mode = Mode_e::Idle;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mode = static_cast<Mode_e>(static_cast<int>(mode) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
#define GENERATE_MODE_STRING(STRING) #STRING,
|
|
||||||
char static const *mode_str[] = {FOREACH_MODE(GENERATE_MODE_STRING)};
|
|
||||||
#endif /*DEBUG*/
|
|
||||||
|
|
||||||
/* Leds */
|
|
||||||
uint16_t static const LedCount{300};
|
|
||||||
|
|
||||||
/* Colors */
|
|
||||||
uint32_t const Red{0x00FF0000};
|
|
||||||
uint32_t const Green{0x0000FF00};
|
|
||||||
uint32_t const Blue{0x000000FF};
|
|
||||||
|
|
||||||
/* Global variables */
|
|
||||||
/*------------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
/* State machine */
|
|
||||||
Mode_e currentMode{Mode_e::Idle};
|
|
||||||
|
|
||||||
/* Feedback led state */
|
|
||||||
bool feedbackLedState{HIGH};
|
|
||||||
|
|
||||||
/* IO Objects */
|
|
||||||
Adafruit_NeoPixel ledBar{LedCount, LedPin, NEO_GRB + NEO_KHZ800};
|
|
||||||
|
|
||||||
/* debug Serial definition */
|
|
||||||
#if defined(__PLATFORMIO_BUILD_DEBUG__)
|
|
||||||
#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_PROMICRO16)
|
|
||||||
Serial_ &debugSerial = Serial;
|
|
||||||
#else
|
|
||||||
#warning "No serial defined for debug"
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Private function declarations */
|
|
||||||
/*------------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
bool has_mode_changed(char c);
|
|
||||||
|
|
||||||
/* State machine */
|
|
||||||
#define GENERATE_MODE_EXEC_DECLARTION(MODE) void MODE##_execute(char c);
|
|
||||||
FOREACH_MODE(GENERATE_MODE_EXEC_DECLARTION)
|
|
||||||
|
|
||||||
/* Function definitions */
|
|
||||||
/*------------------------------------------------------------------------------------------------*/
|
|
||||||
void setup() {
|
|
||||||
pinMode(FeedbackLedPin, OUTPUT);
|
|
||||||
digitalWrite(FeedbackLedPin, feedbackLedState);
|
|
||||||
#if defined(__PLATFORMIO_BUILD_DEBUG__)
|
|
||||||
debugSerial.begin(9600);
|
|
||||||
while (!debugSerial.available())
|
|
||||||
;
|
|
||||||
debugSerial.println("Init ... ");
|
|
||||||
#endif /*DEBUG*/
|
|
||||||
|
|
||||||
ledBar.begin();
|
|
||||||
ledBar.setBrightness(50);
|
|
||||||
ledBar.show(); // Initialize all pixels to 'off'
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned i = 0;
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
char c = debugSerial.read();
|
|
||||||
|
|
||||||
// if (c == '\n') {
|
|
||||||
// if (i >= LedCount) {
|
|
||||||
// i = 0;
|
|
||||||
// }
|
|
||||||
// debugSerial.print("Lighting ");
|
|
||||||
// debugSerial.println(i, DEC);
|
|
||||||
// ledBar.setPixelColor(i++, ledBar.Color(100, 0, 0));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
if (has_mode_changed(c)) {
|
|
||||||
ledBar.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (currentMode) {
|
|
||||||
#define GENERATE_MODE_EXEC_CALL(MODE) \
|
|
||||||
case Mode_e::MODE: \
|
|
||||||
MODE##_execute(c); \
|
|
||||||
break;
|
|
||||||
FOREACH_MODE(GENERATE_MODE_EXEC_CALL)
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ledBar.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool has_mode_changed(char c) {
|
|
||||||
Mode_e previousMode = currentMode;
|
|
||||||
switch (c) {
|
|
||||||
case 'y':
|
|
||||||
currentMode = Mode_e::Idle;
|
|
||||||
break;
|
|
||||||
case 'u':
|
|
||||||
currentMode = Mode_e::Chaser;
|
|
||||||
break;
|
|
||||||
case 'i':
|
|
||||||
currentMode = Mode_e::Rainbow;
|
|
||||||
break;
|
|
||||||
case 'o':
|
|
||||||
break;
|
|
||||||
case 'h':
|
|
||||||
break;
|
|
||||||
case 'j':
|
|
||||||
break;
|
|
||||||
case 'k':
|
|
||||||
break;
|
|
||||||
case 'l':
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return currentMode != previousMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Idle_execute(char c) {
|
|
||||||
switch (c) {
|
|
||||||
case 'r':
|
|
||||||
debugSerial.println("\nLighting all in red");
|
|
||||||
ledBar.fill(Red);
|
|
||||||
break;
|
|
||||||
case 'g':
|
|
||||||
debugSerial.println("\nLighting all in green");
|
|
||||||
ledBar.fill(Green);
|
|
||||||
break;
|
|
||||||
case 'b':
|
|
||||||
debugSerial.println("\nLighting all in blue");
|
|
||||||
ledBar.fill(Blue);
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
debugSerial.println("\nReset");
|
|
||||||
ledBar.clear();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
|
|
||||||
} chaser_state_t;
|
|
||||||
|
|
||||||
void Chaser_execute(char c) { (void)c; }
|
|
||||||
void Rainbow_execute(char c) { (void)c; }
|
|
Loading…
Reference in New Issue
Block a user