#arduinouno — Public Fediverse posts
Live and recent posts from across the Fediverse tagged #arduinouno, aggregated by home.social.
-
-
Biofeedback Butterfly Beats With a Pulse
https://fed.brid.gy/r/https://hackaday.com/2026/05/19/biofeedback-butterfly-beats-with-a-pulse/
-
Bu fotoğrafları tekrar paylaşmak istiyorum.
Bu benim iki Arduino UNO R3'üm. Birinin üzerinde 2.8 Dokunmatik Ekran takılı!Artık Arduino'yu Qualcomm satın aldı ve ikonik Arduino UNO'nun sonuna yeni sahibini işaret eden "Q" harfini ekleyip Debian Linux çalıştıran bir tek kart bilgisayara dönüştürdüler!
Henüz dönüşüm aşaması tamamlanmadı ve ben de tüm Arduino severler gibi sonucu bekliyorum!
Eğer iyi olduğuna inanırsam 8GB RAM olanı çıkınca alabilirim.
Eğer inanırsam!
-
KernelUNO, An OS For The Arduino Uno
https://fed.brid.gy/r/https://hackaday.com/2026/04/23/kerneluno-an-os-for-the-arduino-uno/
-
Minimalist Lo-Fi Minimalism – Part 2
Last year I programmed up an Arduino with my Pi Day MIDI Sequencer to play a short extract from Philip Glass’ Einstein on the Beach. You can read all about that here: Minimalist Lo-Fi Minimalism.
Having spent a fair bit of the time since then playing around with an Arduino and SP0256A-AL2 and finally getting it hooked up to MIDI, I parked an idle though that it might be interesting to get it “singing” part of Einstein on the Beach. This post comes back to that idea.
https://makertube.net/w/4upxhBNemynPiFKfdzzea2
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
These are the key Arduino tutorials for the main concepts used in this project:
- Minimalist Lo-Fi Minimalism
- SP0256A-AL2 Speech Synthesis
- Arduino and SP0256A-AL2 – Part 6
- Pi Day MIDI Sequencer
If you are new to Arduino, see the Getting Started pages.
Parts list
- 2x Arduino Uno
- Pi Day MIDI Sequencer
- Arduino SP0256A-AL2 Shield Design
The Circuit
I did wonder about combining both the Pi Day sequencer and SP0256A-AL2 shield into a single device, but then decided it would be simpler to treat them as two separate devices and use MIDI to pass control between them.
Consequently I’ve ended up as follows:
- Pi Sequencer -> MIDI TRS -> SL0256A-AL2 -> MIDI TRS -> synth
Where the Pi sequencer is running the code for Minimalist Lo-Fi Minimalism with a small update to send additional MIDI messages to be interpreted by the SL0256A-AL2. All other MIDI is passed through the SL0256A onto a MIDI synth as before.
The Code
Having decided on the basic architecture, the next decision was how to encode the information destined for the speech synthesizer. I need to be able to tell it a pitch and a number to be “sung”. I toyed with the following ideas:
- MIDI NoteOn with pitch, but using note velocity to encode the number to sing.
- MIDI Program Control to select the number, then MIDI pitch and velocity as normal.
- Use the MIDI channel for the number, and pitch and velocity as normal.
I opted for the last option, treating each number as a different instrument on a different MIDI channel. This is a bit wasteful but was a lot easier for testing than attempting to get a specific velocity or having to send MIDI PC messages to keep reselecting the “voice” before each note.
As described previously in Minimalist Lo-Fi Minimalism MIDI channels 1,2 and 3 are already used for the bass and two voice lines, so I’m using MIDI channels 4 through to 13 for the numbers one to ten.
The code for the Glass already has pitch and which number encoded into the data structures. To trigger the “singing” via the speech synthesizer I’m using the Soprano voice pitches, but an octave lower, so it is a relatively simple matter of adding in an additional MIDI send as follows:
MIDI.sendNoteOn(sop[i], 64, MIDI_CHANNEL_SOP); // Original code
MIDI.sendNoteOn(sop[i]-12, 64, MIDI_CHANNEL_NUM-1+num[i]); // New code
lastsop = sop[i]; // Original codeIt is using the “NUM” MIDI channel – 1 as I’m encoding the numbers 1 to 10 which are stored in the num[] array. As the speech synthesis is essentially running at a fixed duration for each utterance, I’m not even bothering with a Note Off message.
On the speech synth side, again essentially all of the code is the same as for the Arduino and SP0256A-AL2 – Part 6 MIDI code, but instead of singing “do, re, mi”, etc linked to pitch, I need to take the word from the MIDI channel. To do this, I’ve expanded the speak() function to include the number and call it as follows from the NoteOn callback:
speak(channel-MIDI_CH2NUM, pitch);
This will result in a number from 1 to 10 and a MIDI pitch which can then be used to select the playback frequency as before and then say the allophones corresponding to the received number.
void speak (uint8_t num, uint8_t note) {
midi2clock (note);
switch(num){
case 1: // One
spAllo(WW1);
spAllo(AX1);
spAllo(NN1);
break;
case 2: // Two
spAllo(TT2);
spAllo(UW2);
break;
}
spAllo(PA3);
}The SP0256A-AL2 datasheet lists the allophones to use for the basic numbers.
I’ve used, in a few cases, slightly shortened versions of the numbers from one to ten. In particular I’ve removed the duplicate allophones for six and seven to make them a little shorter to playback.
The allophones for “One” includes the use of “SX” but I can find no other mention of that in the datasheet, so I’ve ignored it.
One final change was to tweak the timings of the original playback. I’ve had to slow it down a lot to give the SP0256A-AL2 time to say each number, and I’ve also introduced a small delay between sending the MIDI messages and updating the numbers on the display to allow them to sync up a little better.
If the MIDI went to the synth directly from the Pi Day sequencer then a delay would probably be required there too, but as it has to go through the SP0256A-AL2 and get sent back out using the “software THRU” of the Arduino MIDI library, it has a natural slight delay already and isn’t too noticeably out of sync.
Closing Thoughts
I always knew there would be a few limitations, not least of which due to the time it takes to play back the allophones, but in essence I believe this works. I’d rather it was a little more up tempo, but sometimes one just has to work with what is available.
I think it is certainly keeping within the spirit of attempting an extract of the original opera on 8-bit microcontrollers, so it’s not doing too badly.
Kevin
#arduinoUno #midi #Minimalism #PhilipGlass #piday #sp0256aal2 -
I've persuaded my SP0256A-AL2 to "sing" the Twelve Days of Christmas.
https://diyelectromusic.com/2025/12/23/sp0256a-al2-sings-the-twelve-days-of-christmas/
#SP0256AAL2 #ArduinoUno #MusicAdventCalendar #MyApologies #FiveGoldRings
-
SP0256A-AL2 Sings the Twelve Days of Christmas
Partly prompted by a programming challenge by Futzle on Mastodon, I thought it might be interesting to try to get my Arduino and SP0256A-AL2 to sing the Twelve Days of Christmas.
https://makertube.net/w/diBkzrhkh3tekAPBhGzrVj
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
These are the key Arduino tutorials for the main concepts used in this project:
If you are new to Arduino, see the Getting Started pages.
Parts list
- Arduino Uno
- Arduino SP0256A-AL2 Shield
The Code
I wasn’t aiming for a clever code solution, as can be seen submitted to the programming competition already, but I was hoping to get the computer to do at least some of the work for me.
The first issue is getting a useful range for the song. My original MIDI SP0256 project managed around an octave and a half. Well it so happens that the 12 Days of Christmas is just over 1 octave, so if I can position that to the top end of the range, I should be ok.
This is the most compact notation of the tune that I know of. This is from the “Christmas Praise” Brass Band carol book, which seems to be one of the most common references for any band out caroling at this time of year.
Ignoring the introduction, I’m going to transpose this into concert Bb (i.e. up a minor third) which gives me a note range from F2 through to G3. This allows me to use a sensible range of the SP0256A without it getting too slow down at the bottom end of things.
The main text/lyric structure will be set up in a series of functions as follows:
intro() - "on the nth day of Christmas my true love gave to me"
nth() - "first", "second", etc.
one() - "a partridge in a pear tree"
two() - "two turtle doves"
...
twelve() - "twelve drummers drumming"Some variations are required as follows:
- intro() – needs to be able to switch to “and a partridge” for all verses apart from verse 1.
- two(), three(), four() – have a different melody from verse 5 onwards.
- five() – ideally this should get slower and more drawn out with each repetition.
- one() – ideally this would slow down on the last time through.
In order to capture the variations and repeats for each verse, I’ve opted to use an increasing bitmask for each time through to trigger the various phrases. Bit 0 always indicates one() must be called. Bit 1 indicates two(), and so on. It starts with the value 1 but will keep adding another bit as the verses change, so will have the values 1, 3, 7, 15, 31, 63, etc. thus allowing me to build up the verses with one bit per verse.
one() takes bool parameters to indicate first time and last time through. two(), three(), four() take a bool parameter to indicate if it is a “post five gold rings” verse. Using the bitmask this is pretty easy as it just means any of the bits 0x0FF0 will be set for verses 5+.
Here is the main code loop.
uint16_t phrase=1;
for (int verse=1; verse<=12 ; verse++) {
intro(verse);
delay(200);
if (phrase & 0x0800) twelve();
if (phrase & 0x0400) eleven();
if (phrase & 0x0200) ten();
if (phrase & 0x0100) nine();
if (phrase & 0x0080) eight();
if (phrase & 0x0040) seven();
if (phrase & 0x0020) six();
if (phrase & 0x0010) five(verse);
if (phrase & 0x0008) four(phrase & 0x0FF0);
if (phrase & 0x0004) three(phrase & 0x0FF0);
if (phrase & 0x0002) two(phrase & 0x0FF0);
if (phrase & 0x0001) one(phrase == 1, verse == 12);
phrase = (phrase<<1) + 1;
delay(1000);
}The timings were really quite tricky as I have several variables to work to:
- Each allophone takes a different time to be said.
- Each allophone also has a recommended delay time (this might be to account for the time to say them – I’m not sure).
- When I change the frequency, the time to say any specific allophone also changes, with lower frequencies significantly slower than higher ones.
- I naturally need to account for the musical rhythm changes too.
In principle I could probably work out the slowest interval and work back from there to get some accurate timings, but I just did a bit of trial and error on each phrase until it was close enough. The unevenness remaining was because I didn’t want to slow the whole thing down to the slowest phrases being “sung”. It is slow enough already!
Also, as it doesn’t look much, I’ve added an LED to light up at the start of an allophone and to go off whenever a pause is encountered.
Closing Thoughts
I’m actually really pleased with this. It is a good balance of close enough to show the principle without me undertaking complex timing and endless fiddling.
I could probably get a bit cleverer with the code, but that wasn’t really the point for me. I’ve used an algorithm where it mattered to me, and was quite happy to spell out the allophones, frequencies, and timings by hand as I went.
Lastly, let me wish you the compliments of the season, to you and yours.
Kevin
-
Atari Synth Cart Controller
This project uses my Atari 2600 Controller Shield PCB in reverse, to allow the Arduino to act as an Atari keypad controller and thus allow it to control the Atari Synth Cart.
https://makertube.net/w/ryCciwFyQQpcs1Q4Wwy52x
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
These are the key Arduino tutorials for the main concepts used in this project:
If you are new to Arduino, see the Getting Started pages.
Parts list
- Arduino Uno
- Atari 2600 Controller Shield PCB Revisited
- Arduino Stackable TRS MIDI Interface
- DB9 to DB9 cable
- MIDI controller and Atari 2600 with Synth Cart
The Circuit
In this use of my Atari 2600 Controller Shield PCB the 9-pin d-type connectors are directly connected to the Atari console.
There is one issue however. Which device should provide 5V? The Atari pinout has 5V present on pin 7 and this is hooked up to the Arduino 5V line.
It may be possible to run the Arduino off the Atari 5V line, which would be really convenient if so. But I’ve not found a definitive statement of the maximum current draw through the 5V pin of the d-type connector from an Atari 2600.
From the schematics for the 2600, it appears that the 5V comes directly from a 7805 regulator. This may or may not go via a single inductor depending on what version of the schematic I’m looking at (yes, for the schematics on Atari Age, below, no for the schematic in the field service manual).
From the BOM in the field service manual, this would appear to be a 78M05 which I believe has a maximum current output of 500mA with a suitable heatsink.
But that has to power the entire console of course. In a not particularly scientific measurement, I believe my 2600 jnr was drawing around 360mA and my “woody” perhaps around 320mA. I don’t know how this changes with different games or controllers.
According to this post, an Arduino Uno with no IO pin current draw, will pull around 50mA when powered from the 5V line directly. Curiously, a bare-bones ATMega328P can apparently drop to almost 15mA when not driving regulators, a USB chip and a power LED…
From powering up a number of Arduino projects I had lying around, most hardly registered on a simple USB current meter using 10mA units. Some with LEDs got to show 0.02A. Some with screens got up to 0.2A.
So just thinking about it in a somewhat hand-wavy kind of way, I suspect that powering an Uno would probably be ok…
If not, then the 5V line to the d-type connectors will have to be cut and the Arduino independently powered. But then the signals for the keypad will be set at the Arduino’s idea of what 5V looks like, not the Atari. They ought to be essentially the same, but it can’t be guaranteed.
Ideally to go this route, the Arduino would isolated from the Atari and be switching the Atari’s 5V line on and off for the input signals as required, but that would require a new design of the PCB.
So time for an IMPORTANT WARNING: This could well damage the Atari 2600 console as I’m essentially making it up as I go at this point.
I have a cheap Atari 2600 junior that I picked up a while back that I’m happy to experiment with. I’m not using my own, original “woody”. In the end I used the following:
Volca <– 3.5 mm jack to jack –> Arduino TRS MIDI + Atari Shield <– 9-pin to 9-pin –> 2600
Here is the full setup using a Video Touch Pad as the second controller. Note the Arduino is fully powered from the Atari at this point, but the Volca is running on batteries.
The Code
The basic principle for the Arduino is to monitor the Atari’s “row” signals via INPUT pins and when detected drive any OUTPUTs LOW to indicate keys being pressed. The basic algorithm is as follows:
rows[] = IO INPUT pins connected to D-type pins 1, 2, 3, 4
cols[] = IO OUTPUT pins connected to D-type pins 5, 6, 9
FOREACH row[]:
IF row is scanning LOW THEN
Set cols[] HIGH or LOW in turn according to required keypress
REPEAT for second controllerThe keypad row/column map was shown in Arduino Atari MIDI Keypad and reproduced here:
Active (pressed) keys will show as LOW in a column when that row is scanned, so to emulate 6 being pressed, when row on pin 2 is detected as being LOW, column on pin 6 should be driven LOW and columns on pins 5 and 9 should be driven HIGH.
I need to map these keys over onto MIDI notes. One issue is that the Atari doesn’t have a natural scale as generating the frequencies for notes is pretty limited.
Still, I’ve mapped the 12 keys, with their non-natural scale, over onto MIDI notes 60-71 – i.e. all semitones up from middle C.
I’m using PORT IO in an interrupt driven scanning routine to ensure the Arduino is as responsive as it can be to ROW scanning. I also pre-compute the actual column bit values when a MIDI note is received, meaning the scanning routine only has to write out the preset values.
The main functions that achieve this are shown below, with the PORT IO values set up for the second Atari controller connector on my Atari 2600 Controller Shield PCB.
uint8_t row[ROWS];
uint8_t colbits[COLS] = {
0x02, // C0 = A1 PORTC
0x01, // C1 = A0 PORTC
0x10 // C2 = D12 PORTB
};
void keyOn (int r, int c) {
if (r < ROWS && c < COLS) {
// Clear the bit as need active LOW
row[r] &= (~colbits[c]);
}
}
void keyOff (int r, int c) {
if (r < ROWS && c < COLS) {
// Set the bit as need active LOW
row[r] |= colbits[c];
}
}
void scanKeypad (void) {
// ROWS: D11-D8 = ROW1-ROW4
if ((PINB & 0x08) == 0) { // D11
PORTB = (PORTB & (~0x10)) | (row[0] & 0x10); // COL D12
PORTC = (PORTC & (~0x03)) | (row[0] & 0x03); // COL A0, A1
}
if ((PINB & 0x04) == 0) { // D10
PORTB = (PORTB & (~0x10)) | (row[1] & 0x10);
PORTC = (PORTC & (~0x03)) | (row[1] & 0x03);
}
if ((PINB & 0x02) == 0) { // D9
PORTB = (PORTB & (~0x10)) | (row[2] & 0x10);
PORTC = (PORTC & (~0x03)) | (row[2] & 0x03);
}
if ((PINB & 0x01) == 0) { // D8
PORTB = (PORTB & (~0x10)) | (row[3] & 0x10);
PORTC = (PORTC & (~0x03)) | (row[3] & 0x03);
}
}As the Synth Cart notes are mostly controlled from the left controller, I’m only coding up for the Arduino to drive one controller. I’m using a genuine Video Touch Pad for the second controller.
Closing Thoughts
I really ought to map the pitches of the Atari notes onto their respective MIDI notes, but then I’m not sure what to do for the gaps.
In principle I could wire up both controllers and then use MIDI pads on a MIDI controller just as MIDI-driving control keys rather than actual keyboard notes, but this shows the principle.
I’m still not sure about the power issues, but it seems to work. I guess it will keep working until one day it might not and I’ll be looking for a new Atari 2600 junior.
The slight stuttering in the video is when I end up touching a couple of the volca’s keys at the same time. I suspect I could do something a bit more robust in code to prevent repeat triggering, but this is all fine for now as a proof of concept.
Kevin
-
terra has @[email protected] app lab for the uno q now
you can grab it from your app store right now! we hope you have a fun time with it and your uno q
if you'd rather have a bad time you should look at how we had to build this thing
https://github.com/terrapkg/packages/tree/frawhide/anda/tools/arduino-app-lab-bin
#tech #linux #terra #fyralabs #arduino #unoq #arduinounoq #arduinouno #arduinoapplab #fedora #ultramarinelinux -
TD4 4-bit DIY CPU – Part 7
Once the idea was floated, in Part 6 of creating an Arduino “direct to ROM” assembler, I had to just do it, so this post is a little diversion from the hardware discussion into how that could work.
- Part 1 – Introduction, Discussion and Analysis
- Part 2 – Building and Hardware
- Part 3 – Programming and Simple Programs
- Part 4 – Some hardware enhancements
- Part 5 – My own PCB version
- Part 6 – Replacing the ROM with a microcontroller
- Part 7 – Creating an Arduino “assembler” for the TD4
- Part 8 – Extending the address space to 5-bits and an Arduino ROM PCB
Basic Concepts
This relies on using an Arduino as the ROM as described in Part 6, but the Arduino now has the option to change the ROM contents independently of the TD4 itself.
The Arduino sketch will do the following:
- Run the TD4 ROM routine off a timer interrupt so that it is always running and responsive.
- Take input over the Arduino serial port to allow basic control, e.g. list, clear, etc.
- Allow the direct input of assembler instructions, such as MOVE A,B or OUT B and so on.
- Provide a means of selecting which line of the program to change.
The code will thus have a number of key sections:
- The TD4 ROM routine.
- Some kind of serial-port command-line interpreter.
- Handler routines for all the commands.
- An assembler.
- A disassembler.
The TD4 ROM routine has already been fully described in Part 6. The only difference is that the scanning routine will be driven from a 1mS timer using the TimerOne library.
As I want to still support a built-in demo, I now have the concept of ROM being the demo code and RAM being the “live” code to pass onto the TD4. The Arduino will initialise the RAM on startup from the ROM.
As far as the TD4 is concerned of course, this is all still ROM.
Command Line Interpreter
The standard Arduino Serial routines will be used to scan for input via the serial port. It will support a line-oriented input as follows:
bool cmdRunner (void) {
while (Serial.available()) {
char c = Serial.read();
if (c == '\n') {
strcpy(cmdSaved, cmdInput);
cmdIdx = 0;
return true;
}
else if (cmdIdx < CMD_BUFFER-1) {
cmdInput[cmdIdx++] = c;
cmdInput[cmdIdx] = '\0';
}
}
return false;
}This will keep adding any received characters to the cmdInput buffer until a newline is received, at which point the command is saved in cmdSaved and the routine will return true indicating a full line is ready to be processed.
Once a complete line is received, then a processing function will parse it.
Key to the processing of commands is a command table that stores the text to match and the handler function to call on finding a valid command. There is an additional parameter that will be passed into the handler function to allow the same handler function to support several commands. This will be used in the assembler itself later.
struct cmd_t {
char cmd[CMD_BUFFER+1];
hdlr_t pFn;
uint8_t idx;
};
const cmd_t PROGMEM cmdTable[NUM_CMDS] = {
{"H", hdlrHelp, 0},
{"L", hdlrList, 0},
{"G", hdlrGoto, 0},
};The algorithm for parsing commands is as follows:
cmdProcess:
Look for a space or newline
IF found a space THEN
This is the start of the parameter
Look for the command in the command table
IF command found THEN
Call the handler function with the parametersThe implementation is a bit complex, as it uses string pointers and has to chop and parse strings as it goes. It is also detailing with the command table in the Arduino’s PROGMEM which is an additional complication too.
In order to be able to use the same command line interpreter for the input of assembler instructions, I’ve had to simplify the syntax. There are no spaces in opcodes and there has to be a space between the opcode and immediate value if used.
Here are some examples:
IN A -> INA
MOVE A,B -> MOVAB
OUT im -> OUT im
JNC im -> JNC im
ADD A,im -> ADDA imHandler Routines
All handler routines have the following prototype:
typedef void (*hdlr_t)(int idx, char *param);
void hdlrHelp(int idx, char *pParam) {
Serial.print("\nHelp\n----\n");
Serial.println("H: Help");
}The idx parameter is the number in the last field of the command table. pParam will be a pointer to the parameter string for the command (if used).
As we’re dealing with strings all the time, there are a number of helper functions to do things like convert strings to numbers as well as others to print numbers in various formats.
Number formats are assumed to be as follows:
0..9 - decimal digits
0x0..F - hex digits
b0..1 - binary digitsThe code provides the following:
- str2num – the basic string parsing routine to recognise all three number formats as strings.
- printbin – print a number in b0..1 format.
- printhex – print a number in 0x0..F format, allowing for a possible leading zero if required.
- printins – print an instruction in textual format.
- printop – print an instruction in binary and hex opcode format.
- printline – print a line number in a consistent binary and hex format.
The code supports the following commands, so each has its own handler function:
- H – help – show the list of commands.
- L – list – show the disassembly of the whole working memory (RAM).
- G – goto – set the working line number.
- C – clear – reset all working memory (RAM) to zeros.
- R – restore – restore the working memory (RAM) to the pre-build demo code (ROM).
- O – opcodes – list the supported opcodes.
Assembler
As already mentioned, I’m using the same command line interpreter code to create the assembler. To do this, each opcode has an entry in the command table:
const cmd_t PROGMEM cmdTable[NUM_CMDS] = {
// Assembly commands - must be first
{"ADDA", hdlrAsm, 0},
{"MOVAB", hdlrAsm, 1},
{"INA", hdlrAsm, 2},
{"MOVA", hdlrAsm, 3},
{"MOVBA", hdlrAsm, 4},
{"ADDB", hdlrAsm, 5},
{"INB", hdlrAsm, 6},
{"MOVB", hdlrAsm, 7},
{"OUTB", hdlrAsm, 8},
{"OUT2B", hdlrAsm, 9},
{"OUT", hdlrAsm, 10},
{"OUT2", hdlrAsm, 11},
{"JNCB", hdlrAsm, 12},
{"JMPB", hdlrAsm, 13},
{"JNC", hdlrAsm, 14},
{"JMP", hdlrAsm, 15},
// Other commands
{"H", hdlrHelp, 0},
{"L", hdlrList, 0},
{"G", hdlrGoto, 0},
{"C", hdlrClear, 0},
{"R", hdlrRestore, 0},
{"O", hdlrOpcodes, 0},
};The order corresponds to the opcode command value, as does the parameter. As these are at the start of the table, I can assume that the position in the table is the same as the command value. This does mean that I also need to account for the duplicated instructions even if I don’t need to use them.
I’m making the following design decisions:
- There is the concept of a “current line” which can be set with the G (goto) command.
- Entering a valid opcode automatically moves the current line on by 1.
- No line information is entered as part of the opcode.
The main logic of the assembler handler is as follows:
Assembler:
Command value is the provided index parameter
Determine the im value from the provided string parameter
RAM[line] = cmd << 4 + im
Increment current lineDisassembler
Disassembly is really largely a look-up table matching opcode command values to text. This is all hidden away behind the two print routines printins() and printop().
void printins (uint8_t ins) {
uint8_t cmd = ins >> 4;
uint8_t im = ins & 0x0F;
Serial.print(FSH(cmdTable[cmd].cmd));
if (HASIM(cmd)) {
Serial.print(" b");
printbin(im,4);
} else {
Serial.print(" ");
}
}
void printop (uint8_t op) {
uint8_t cmd = op >> 4;
uint8_t im = op & 0x0F;
Serial.print("b");
printbin(cmd,4);
Serial.print(" ");
printbin(im,4);
Serial.print("\t0x");
printhex(op,2);
}The main complexity is pulling the strings out of the command table. I’ve had to include a macro to provide access to the strings from the Arduino’s PROGMEM:
#define FSH(x) ((const __FlashStringHelper *)x)
This feels like a bit of a hack, but apparently this is how it should be done for the kind of thing I need to do!
There is another macro here that needs explaining:
#define HASIM(op) (op==0||op==3||op==5||op==7||op>9)
This is a set of conditions that if true means that the command supports an immediate value. This is used in a few places to know how to parse the commands.
Whilst in principle all commands could use the immediate value, the “official” statement of how they work assumes im=0 in many cases. So, for example, OUT B does not require an immediate value, but if one is provided then OUT B becomes OUT B+im.
I’m not really supporting that with this code at the moment.
Putting it all together
Here is a serial output log of a session using the assembler.
> H
Help
----
H: Help
L: List
G: Goto
C: Clear
R: Restore
O: Opcodes
OpCode
OpCode im
Current line: b0000 [0]
> L
RAM Disassembly
b0000 [0]: JNC b1000b1110 10000xE8
b0001 [1]: JMP b0011b1111 00110xF3
b0010 [2]: OUT b0010b1010 00100xA2
b0011 [3]: ADDB b0001b0101 00010x51
b0100 [4]: OUT b0100b1010 01000xA4
b0101 [5]: ADDA b0001b0000 00010x01
b0110 [6]: OUT b1000b1010 10000xA8
b0111 [7]: ADDB b0001b0101 00010x51
b1000 [8]: OUT b0100b1010 01000xA4
b1001 [9]: ADDA b0001b0000 00010x01
b1010 [A]: OUT b0010b1010 00100xA2
b1011 [B]: ADDB b0001b0101 00010x51
b1100 [C]: JMP b0000b1111 00000xF0
b1101 [D]: ADDA b0000b0000 00000x00
b1110 [E]: ADDA b0000b0000 00000x00
b1111 [F]: ADDA b0000b0000 00000x00
Current line: b0010 [2]
> G 13
Goto line 13
Current line: b1101 [D]
> OUTB
Assemble:
b1101 [D] OUTB b1000 00000x80
Current line: b1110 [E]
> L
RAM Disassembly
b0000 [0]: JNC b1000b1110 10000xE8
b0001 [1]: JMP b0011b1111 00110xF3
b0010 [2]: OUT b0010b1010 00100xA2
b0011 [3]: ADDB b0001b0101 00010x51
b0100 [4]: OUT b0100b1010 01000xA4
b0101 [5]: ADDA b0001b0000 00010x01
b0110 [6]: OUT b1000b1010 10000xA8
b0111 [7]: ADDB b0001b0101 00010x51
b1000 [8]: OUT b0100b1010 01000xA4
b1001 [9]: ADDA b0001b0000 00010x01
b1010 [A]: OUT b0010b1010 00100xA2
b1011 [B]: ADDB b0001b0101 00010x51
b1100 [C]: JMP b0000b1111 00000xF0
b1101 [D]: OUTB b1000 00000x80
b1110 [E]: ADDA b0000b0000 00000x00
b1111 [F]: ADDA b0000b0000 00000x00
Current line: b1110 [E]
> O
Supported OpCodes:
b0000 dataADDA im
b0001 0000MOVAB
b0010 0000INA
b0011 dataMOVA im
b0100 0000MOVBA
b0101 dataADDB im
b0110 0000INB
b0111 dataMOVB im
b1000 0000OUTB
b1001 0000OUT2B
b1010 dataOUT im
b1011 dataOUT2 im
b1100 dataJNCB im
b1101 dataJMPB im
b1110 dataJNC im
b1111 dataJMP im
> C
Clearing RAM ... DoneConclusion
The basics for this actually came together fairly quickly, but I must admit to spending a fair bit of time fiddling about with output formats and refactoring various bits of code to try to give some consistency in terms of when newlines are applied, what is shown in binary, what in hex, and so on.
I can’t guarantee everything has been caught, but I’ve typed in all the code (using the newer, limited syntax) from Part 3 and they all seem to work.
It would be nice to be able to automatically reset the TD4 from the Arduino, but for now, pressing the button when required is fine.
For the most part, unless there is a loop to get caught in, the code will cycle back to the start anyway.
In terms of possible updates and enhancements, there are a few on my mind:
- It would be nice to support the undocumented use of immediate values somehow.
- It might be nice to have a way to save/load the code. It only needs to be a string of 16 2-byte hex codes.
- It might be nice to have several demo programs to choose from.
If I expand the instruction set and architecture, then I’ll have to think again about chunks of this code, but for now, it seems to work pretty well.
Kevin
-
Amiga? Arduino? AMeagerBall Gets the Uno Bouncing https://hackaday.com/2025/11/19/amiga-ardunio-amegerball-gets-the-uno-bouncing/ #Microcontrollers #ArduinoUno #boingball #demoscene
-
TD4 4-bit DIY CPU – Part 6
Having now successfully built my own version of the TD4 4-bit CPU in Part 5, I’m now chewing over some of the ways I’d like to try to expand it.
- Part 1 – Introduction, Discussion and Analysis
- Part 2 – Building and Hardware
- Part 3 – Programming and Simple Programs
- Part 4 – Some hardware enhancements
- Part 5 – My own PCB version
- Part 6 – Replacing the ROM with a microcontroller
- Part 7 – Creating an Arduino “assembler” for the TD4
- Part 8 – Extending the address space to 5-bits and an Arduino ROM PCB
I already have a list of others extended projects at the end of Part 4, so I might be drawing on some of them for inspiration moving forward. Many of these are very similar projects, but with a completely different architecture. But really at this stage rather than build a different, more capable, 4-bit CPU from someone else’s design, I’m interested in seeing how far the TD4 design can go. So, ultimately, like all my projects, the fun here is in the reinventing and learning on the way.
One of the questions I have is can I replace the DIP switches with something that can provide the data in a better way? This would be particularly critical if I expand the address space in the future. A ROM is the obvious option, but something more dynamic might be an interesting experiment too.
This post looks at options for replacing the DIP switches with microcontrollers.
Now I feel like I really ought to state right up front that this is a pretty ludicrous thing to do.
At the more charitable end of the endeavor I’m using a 16MHz 8-bit AVR microcontroller with 2kB of RAM to serve up 16 8-bit values to a 10Hz 4-bit CPU.
At the most extreme end I’m using a 125MHz, dual-core, 32-bit ARM Cortex M0+ CPU with 264 kB of RAM running an entire interpreted programming environment requiring (probably) millions of lines of low-level code to implement it, to do the same thing.
So why bother? Well – why not?
TD4 without the ROM
To interface to a microcontroller, I’m after two things:
- Ability to read the 4 address lines.
- Ability to drive the 8 data lines.
The best place to get at these signals is on the interface to the ROM itself – the 74HC540 octal line driver, and 74HC154 4-to-8 line decoder.
Conveniently, these signals can be broken out quite easily on my board as shown below.
The pink shaded area shows which components are needed for a ROM-less build. The two yellow highlights show where headers should be soldered to permit access to the address lines (top) and data lines (bottom).
In this build, the following components are omitted from the full board:
- 74HC154
- 74HC540
- 16x 8-way DIP switches
- 128x small signal diodes
- 8x 10k pull-up resistors
I’ve used 6-way and 10-way pin header sockets to allow me to patch in a microcontroller. This allows for each header to conveniently include 5V and GND too. I’ve included the USB socket for power to the PCB but expect I’ll probably power the board via these 5V and GND links from the microcontroller.
Using Arduino
The natural choice here is to use one of the older Arduino boards, as these are all 5V IO which makes interfacing with the 4-bit CPU fairly straight forward.
Using Arduino direct PORTIO should also make it pretty trivial to read address lines and write the data. I’ve configured the connections as follows:
TD4 SignalArduino GPIOArduino PORTIOA0A0PORTC:0A1A1PORTC:1A2A2PORTC:2A3A3PORTC:3D0D8PORTB:0D1D9PORTB:1D2D10PORTB:2D3D11PORTB:3D4D4PORTD:4D5D5PORTD:5D6D6PORTD:6D7D7PORTD:7I’m avoiding D0/D1 (PORTD[0:1]) and D13 as they all have other hardware attached (serial port and LED in this case).
Accessing the data corresponding to any specific address is as simple as follows:
uint8_t ROM[16];
loop:
unt8_t addr = PINC & 0x0F
PORTB = (PORTD & ~(0x0F)) | (ROM[addr] & 0x0F);
PORTD = (PORTD & ~(0xF0)) | (ROM[addr] & 0xF0);The code could be simplified if I didn’t mind trashing whatever is configured for the other GPIO pins via the PORTIO, but it is good practice to preserve those values when only writing to a subset of the IO ports.
In the final code below, I’ve included a toggle for A5 which allows me to do some timing measurements too.
uint8_t ROM[16] = {
0xB1, 0x01, 0xB2, 0x51,
0xB4, 0x01, 0xB8, 0x51,
0xB4, 0x01, 0xB2, 0x51,
0xF0, 0x00, 0x00, 0x00
};
void setup() {
DDRB |= 0x0F;
DDRD |= 0xF0;
DDRC |= 0x20;
}
int toggle;
void loop() {
if (toggle == 0) {
toggle = 1;
PORTC |= 0x20;
} else {
toggle = 0;
PORTC &= ~(0x20);
}
uint8_t addr = PINC & 0x0F;
PORTB = (PORTD & ~(0x0F)) | (ROM[addr] & 0x0F);
PORTD = (PORTD & ~(0xF0)) | (ROM[addr] & 0xF0);
}Running the code in a loop like this gives a scan frequency of around 500kHz and a response time of something like 2-3 uS for each read. That seems pretty responsive and I’m sure will be fine for a 10Hz CPU. And it is – it works great!
Using Circuitpython
One thing that would be really nice is a workflow that allows more of a “direct save to the CPU” approach to programming it. One option is to use a more modern microcontroller that supports a filesystem.
The obvious choice here is a 32-bit microcontroller that supports Circuitpython. But will IO in Circuitpython be fast enough to respond to the CPU? There is one obvious way to find out – give it a try.
There is another complication too – most Circuitpython boards run at 3.3V not 5V so that needs to be addressed too.
Level Shifting
I’m going to use a 74LVC245. The Adafruit product page puts it best:
“essentially: connect VCC to your logic level you want to convert to (say 3.3V), Ground connects to Ground. Wire OE (output enable) to ground to enable the device and DIR (direction) to VCC. Then digital logic on the A pins up to 5V will appear on the B pins shifted down to the VCC logic.”
This is an 8-way bi-directional bus transceiver and should be powered by 3V3, then the direction pin will determine the direction of the conversion as shown[ below.
Two devices will be required. The address lines will need a 5V to 3V3 conversion; the data lines will need 3V3 o 5V.
Here is how I’ve wired these up for a Raspberry Pi Pico:
The Pico is connected as follows:
- INPUT: GPIO 10-13 = A0-A3
- OUTPUT: GPIO 2-9 = D7-D0 (not the ordering!)
CircuitPython ROM
The basic algorithm will be as follows:
ROM = [16 command byte values]
LOOP:
Read four address lines
Set data lines from ROM[address]For performance reasons it would be best to optimise both the reading of the address lines and the writing of the data lines, ideally into a single access. But as this is for a CPU that runs at a maximum of 10Hz, so for now, I’m just going with simple and see how it goes.
import board
import digitalio
ROM = [
0xB1, 0x01, 0xB2, 0x51,
0xB4, 0x01, 0xB8, 0x51,
0xB4, 0x01, 0xB2, 0x51,
0xF0, 0x00, 0x00, 0x00
]
Tpin = digitalio.DigitalInOut(board.GP21)
Tpin.direction = digitalio.Direction.OUTPUT
A0pin = digitalio.DigitalInOut(board.GP10)
A1pin = digitalio.DigitalInOut(board.GP11)
A2pin = digitalio.DigitalInOut(board.GP12)
A3pin = digitalio.DigitalInOut(board.GP13)
D0pin = digitalio.DigitalInOut(board.GP2)
D0pin.direction = digitalio.Direction.OUTPUT
D1pin = digitalio.DigitalInOut(board.GP3)
D1pin.direction = digitalio.Direction.OUTPUT
D2pin = digitalio.DigitalInOut(board.GP4)
D2pin.direction = digitalio.Direction.OUTPUT
D3pin = digitalio.DigitalInOut(board.GP5)
D3pin.direction = digitalio.Direction.OUTPUT
D4pin = digitalio.DigitalInOut(board.GP6)
D4pin.direction = digitalio.Direction.OUTPUT
D5pin = digitalio.DigitalInOut(board.GP7)
D5pin.direction = digitalio.Direction.OUTPUT
D6pin = digitalio.DigitalInOut(board.GP8)
D6pin.direction = digitalio.Direction.OUTPUT
D7pin = digitalio.DigitalInOut(board.GP9)
D7pin.direction = digitalio.Direction.OUTPUT
def doOutput (data):
if (data & 0x01):
D0pin.value = True
else:
D0pin.value = False
if (data & 0x02):
D1pin.value = True
else:
D1pin.value = False
if (data & 0x04):
D2pin.value = True
else:
D2pin.value = False
if (data & 0x08):
D3pin.value = True
else:
D3pin.value = False
if (data & 0x10):
D4pin.value = True
else:
D4pin.value = False
if (data & 0x20):
D5pin.value = True
else:
D5pin.value = False
if (data & 0x40):
D6pin.value = True
else:
D6pin.value = False
if (data & 0x80):
D7pin.value = True
else:
D7pin.value = False
while True:
Tpin.value = True
addr = 0
if (A0pin.value == True):
addr = addr + 1
if (A1pin.value == True):
addr = addr + 2
if (A2pin.value == True):
addr = addr + 4
if (A3pin.value == True):
addr = addr + 8
Tpin.value = False
doOutput(ROM[addr])I’ve included a timing pin to GPIO21 so I can see how long it takes to access the IO.
It turns out that it takes something of the order of 50-60uS to read the four address lines and something in the region of 70-80uS to write out the 8 data lines. The above simple Circuitpython code to do this is running with a frequency of around 7kHz.
Now at this point I ought to be reading through the datasheets for the ICs used in the CPU to check response times and timing tolerances so see if this is ok. But I didn’t bother with any of that as it all appears to work!
Conclusion
The Circuitpython is obviously a lot slower than the Arduino running optimised PORTIO code, even though the Circuitpython is running on a 125MHz processor compared to the Arduino’s 16MHz. Of course, if performance was critical then switching to direct GPIO access in C on the Pico would be a lot faster again. Even just having a way to do a single block-access of GPIO would probably make quite a difference.
But for this application, either as they are seem to work absolutely fine.
The ability to quickly edit the ROM contents is pretty useful with the Circuitpython. But I am now wondering how difficult it would be to have some kind of uploader to the Arduino over the serial port. There are only 16 bytes to transfer after all.
In fact it might even be possible to create a simple interactive assembler that allows code to be typed in over the serial port using proper word-based op-codes (like ADD, IN, OUT, etc). At the very least a simple serial port interface to type in numeric values would be relatively straight forward I think. It might also be possible to allow the microcontroller to reset the CPU too.
I’m not sure the added complications of logic shifting, etc, make it worth carrying on with a Pico version at this stage, so I think improving the Arduino is probably the way to go for now.
Kevin
#4bit #arduinoUno #circuitpython #portio #raspberryPiPico #td4
-
Hackspace LED Panels
I was lucky enough to get some 16×64 LED Panels in a Hackspace giveaway, courtesy of Limehouse Labs, London, but have only recently started to look at how to use them.
As usual, these are my “notes to self” after attempting to use the suggested libraries and not really getting how they work, so I ended up doing what I often do – reinventing this particular wheel so I really understand what is going on.
https://makertube.net/w/uMhG7dcEs9D5BYmsXF7MVK
References:
- https://led.limehouselabs.org/docs/tiles/led-tiles-v2/
- https://github.com/limehouselabs/led-screens
- https://wiki.london.hackspace.org.uk/view/LED_tiles_V2
- https://wiki.hackhitchin.org.uk/index.php?title=LED_Display_Panel_Driving
- https://wiki.hackhitchin.org.uk/index.php?title=LED_panel_interface
- https://wiki.hackhitchin.org.uk/index.php?title=LedPanelEsp32
- https://github.com/neilpstevenson/HHLedDisplayPanels
- https://github.com/JonRussell00/LEDpanels
- https://github.com/krux702/led_panel_wall
- https://github.com/andreacampanella/Mbed_MBI5034_Panel_Driver
And the main devices on the panels are the MBI5034 shift register ICs.
Introduction
The best description of the bit stream required to drive them can be found here: https://wiki.london.hackspace.org.uk/view/LED_tiles_V2
It is a bit complicated…
- There are two address lines, which feed into a HC138 3 to 8 line decoder (the third input is ignored), so together they select one of four blocks of four lines of LEDs.
- There are two data lines into the MBI5034 shift registers, 24 per line, which access the top and bottom half of the display in turn.
- A full stream of data covers two full lines of 64 RGB LEDs.
- Data has to be sent to individual R, G and B LEDs.
The general idea is to send the bit pattern for both D1 and D2 at the same time for a specific address (0, 1, 2 or 3) and keep clocking the appropriate bits out to the device. Then change the address and do it again until all four addresses have been covered.
The address isn’t really an address as such – it is more of a matrix “scan line” in that the LEDs are matrixed so that only the LEDS with the common line corresponding to that “address” will be activated at any one time. As there are four lines, the whole panel can only be scanned using persistence of vision, illuminating each scan line in turn.
To drive multiple panels means adding their D1 and D2 lines to the system too, but sharing all other signals. Most approaches I’ve seen encode all 8 bits or each of the eight Dn lines into a single byte and then sent those out in parallel to four panels at the same time.
If the data is stored in a frame buffer in “4 panel Dn ready format” then driving four panels isn’t really that much slower than driving one.
Some numbers:
- There are 16 x 64 RGB LEDs per panel = 1024 RGB LEDs.
- Each RGB LED is actually three individual R, G and B LEDs, so there are 3072 LEDs to drive per panel.
- Each MBI5034 has 16 outputs, so can drive 16 individual LEDs directly.
- There are 48 MBI5034 chips per panel, 24 per data line, so they have to drive 64 LEDs each…
So on that last point – as already mentioned, the 74HC138 8-line decoder links the address lines A0, A1 to four outputs (Y0, Y1, Y2, Y3). This is used to “select” one of four blocks of 16 LEDs per MBI5034. Once each address has been scanned, each MBI5034 can be seen to be driving the equivalent of 4 rows of 16 LEDs in total, which is the required 64 LEDs each.
Apparently each MBI5034 will directly address a single LED colour: R, G or B. So 48 of these, driving 16 R, 16 B, 16 G means they can directly address 768 LEDs, or 256 R, G, and B LEDs. Again, doing this four times gives the full 1024 RGB LED panel.
This all means that the panel is logically arranged as two 128 x 4 matrices, physically arranged across two rows of 64 LEDs each. The full 16 rows of 64 LEDS are selected by the following combinations of D1, D2, A0, A1:
A0A1D1 = 1D2 = 100Rows 0, 4Rows 8, 1201Rows 1, 5Rows 9, 1310Rows 2, 6Rows 10, 1411Rows 3, 7Rows 11, 15A further complication is that each row itself is encoded not as 2 sets of 64 RGB values, but as 8 sets of [16-bit B + 16-bit R + 16 bit G] values with the 16-bit B, G, R values encoding 8 of each colour LED on the first row, and then 8 on the second row.
The upshot of all this is that 384 bits of data must be streamed out to the shift registers for each address, but only two lines per half-panel can be illuminated at any one time. As I say, persistence of vision is required to illuminate the entire panel.
The basic algorithm required to display something on the panel will thus be as follows:
FOR EACH bank (address = 0, 1, 2, 3):
Clock out 384 bits of BGR data covering two rows of 64 LEDs.
Do this for both D1 and D2 at the same time, so illuminating four rows in total.
IF using more than one panel, also do this for D1 and D2 for all other panels tooHardware
I had some of the interface PCBs made, that were described here: https://github.com/limehouselabs/led-screens/tree/main/panel-connector
The central block is mapped over to a JST PH 8-way connector as follows:
D1LATA1N/CD2OEA0CLKJST PH Connector:
- GND
- CLK
- LAT
- OE
- A1
- A0
- D2
- D1
The board provides screw terminals for a higher current 5V supply.
Various estimates suggest one panel could require up to 8A with all LEDS on at full brightness, so I’m using a 40A 5V supply. The main issue is ensuring the Arduino’s GND and the GND of the power supply are linked, which this PCB does.
Arduino “Shield”
After getting fed up with jumper wires falling out of solderless breadboards, I eventually soldered up a simple Arduino proto shield able to support up to four panels.
This maps the panel signals onto Arduino pins as follows:
ArduinoPanel(s)A0A0 (all panels)A1A1 (all panels)A2CLK (all panels)A3LAT (all panels)A4OE (all panels)D4D5 (D1 panel 3)D5D6 (D2 panel 3)D6D7 (D1 panel 4)D7D8 (D2 panel 4)D8D1 (D1 panel 1)D9D2 (D2 panel 1)D10D3 (D1 panel 2)D11D4 (D2 panel 2)The slightly odd mapping of Arduino digital outputs to panels will become clear when we look at the code.
Using this with a ready-made JST PH connector-to-jumper-wire cable and then crimping Dupont header pins onto it seems to do the job (or so I thought).
Driver Code
I started with the Arduino code linked to above. This is written for an ATMega32U4 based Arduino and uses direct Port IO to access the panels.
I wanted to use an Uno or Nano, so I started to port this over to an ATMega328, but just could not get anything working at all, so in the end I decided to go back to basics and rewrite the code from first principles so I knew what was going on.
The basic design of the code will be as follows:
- An interrupt-driven routine to take a buffer representing each LED that is already in the correct format for the panels, and streams this data out to the panels on a regular basis.
- As mentioned, a frame buffer that can be written to that is already “panel aware”, but has a simple interface using (x, y, colour) to set a pixel.
- Use direct PORTIO where possible to ensure quick updating of the signals when streaming out to the panels.
Driving Four Panels at Once
All signals apart from D1 and D2 are common to both halves of each panel. This means that once the code has processed the address signals, LAT, CLK and OE, there is no reason not to then process D1 and D2 for all existing panels in parallel.
This can be done pretty efficiently on an Arduino by mapping the eight Dn signals onto a single Arduino port and using PORT IO to write out all four values at once.
It isn’t quite that simple however, as there isn’t a single 8-bit port on an Arduino that isn’t used for something else too, so I’m going to do it in two halves, using PORTB and PORTD as follows:
Panel DnArduino DnArduino PORTP1 D18PORTB 0P1 D29PORTB 1P2 D110PORTB 2P2 D211PORTB 3P3 D14PORTD 4P3 D25PORTD 5P4 D16PORTD 6P4 D27PORTD 7Using this mapping, an 8-bit value can be stored in the framebuffer per LED as follows:
b7 6 5 4 3 2 1 0
D8 D7 D6 D5 D4 D3 D2 D1
P4 P3 P2 P1And then when it comes to writing it out via the Arduino digital IO it can be done in two instructions:
PORTB = (PORTB & (~0x0F)) | (data & 0x0F);
PORTD = (PORTD & (~0xF0)) | (data & 0xF0);This is mapping the low 4 bits of the data value onto the low 4 bits of PORTB and the top 4 bits of the data value onto the top 4 bits of PORTD.
The Writing Protocol
Implementing the protocol described in the various references, I’m after the following basic structure:
- Clock out the data on all 8 Dn lines using PORTIO.
- Blank the screen and LATch the data.
- Set A0/A1 accordingly to select the correct row of LEDs via the matrix.
- Unblank the screen.
The entire routine for updating the display is as follows.
void panelScan (void) {
for (int i=0; i<FB_BITS; i++) {
setDataOutput(fb[bank][i]);
// Pulse the clock
CLKSET;
CLKCLR;
}
// Blank display
OESET; // active LOW
// Set address
SETADDR(bank);
// Toggle latch
LATSET;
LATCLR;
// Unblank display
OECLR; // Active LOW
bank++;
if (bank >= FB_BANKS) bank = 0;
}This processes a single address per scan, so four scans are required, with persistence of vision, to illuminate the entire display.
The various macros, such as CLKSET, CLKCLR and so on, are defined for individual bit manipulation PORTIO as follows:
#define CLKSET {PORTC = (PORTC & (~0x04)) | 0x04;} // A2
#define CLKCLR {PORTC = (PORTC & (~0x04)) ;}SETADDR is a special macro that sets both A0 and A1 at the same time.
#define SETADDR(x) {PORTC = (PORTC & (~0x03)) | (x & 0x03);} // A0/A1setDataOutput() is an inline routine that has the two previously mentioned lines in it to set PORTB and PORTD.
The Framebuffer
There is one unexplained item in that previous routine. The value passed into setDataOutput is an entry from the frame buffer – the “panel ready data format” I mentioned previously.
Here is my description of how the framebuffer works from the code itself followed by my setPixel routine that implements it.
// The panel is addressed as a string of bits, representing 64 R,G,B values.
// but the ordering is a little odd.
//
// For each address (A1/A0) a string of bits represents two rows of LEDs.
// 64 * 2 * 3 = 384 bits of information.
//
// Ordering is as follows:
// [ BBBBBBBB BBBBBBBB GGGGGGGG GGGGGGGG RRRRRRRR RRRRRRRR ] * 16
// Row0 0-7 Row4 0-7 Row0 0-7 Row4 0-7 Row0 0-7 Row4 0-7 Repeat for 8-15,16-23,24-31
//
// This addresses rows 0 and 4 when A1/A0 = 00:
// A1 A0 1st Row 2nd Row
// 0 0 0 4
// 0 1 1 5
// 1 0 2 6
// 1 1 3 7
//
// The bit data is streamed out on D1 for the top half of the panel.
// The same principle and format bit stream is streamed out on D2 for the second half.
// Any additional panels will have their own D1/D2 lines and the same format bit streams.
//
// All this means that the (x,y) coordinates must map onto the frame buffer as follows:
// y = b76543210
// ++--- A1/A0 lines 0-3
// +----- First or second row
// +------ D1 or D2
// ++------- Panel number for four panels 0-3
// ++--------- Only required if more than four panels (up to 16)
//
// x = b76543210
// +++--- Position within a block of 8 LEDs
// +++------ Which block of 8 to address
//
// Mapping the x coord is more complex though as the position relates to:
// - 1st/2nd Row determines which block of 8 within each colour.
// - Colour B/G/R determines which pair of blocks of 8.
//
// Led Idx within a block = x[2:0]
// Block Idx in frame buffer = x[5:3] * (16+16+16)
// So LED position = BlockIdx + 8 (if 2nd Row) + 16 (if G) + 32 (if R) + LedIdx
//
// One final complication. The framebuffer is a set of bytes that maps onto
// the different halves of different panels, so which bit in the framebuffer
// gets set depends on the panel number and D1/D2 selection.
//
// Assuming a simple D8-D1 mapping, this means the following:
// b76543210
// ++--- D2/D1 Panel 1
// ++----- D2/D1 Panel 2
// ++------- D2/D1 Panel 3
// ++--------- D2/D1 Panel 4
//
void setPixel (int x, int y, panelcolour col) {
if ((x<0) || (x>=64) || (y<0) || (y>=64)) {
// Only 4x 16*64 panels supported
return;
}
int a1a0 = y & 0x03; // bank
int row4 = y & 0x04 ? 8 : 0;
int d1d2 = y & 0x08;
int panel = y >> 4; // panel 0..15; only 0..4 supported
int bitIdx = y >> 3; // panel no + d1/d2
int led8 = x & 0x07;
int block = x >> 3;
// Determine the bit to act on for the four panels D1-D2
uint8_t bit = (1<<bitIdx);
int off = block * 48 + row4 + led8; // Location of B LED to act on
if (col & BITBLUE) {
fb[a1a0][off] |= bit;
} else {
fb[a1a0][off] &= ~bit;
}
if (col & BITGREEN) {
fb[a1a0][off+16] |= bit;
} else {
fb[a1a0][off+16] &= ~bit;
}
if (col & BITRED) {
fb[a1a0][off+32] |= bit;
} else {
fb[a1a0][off+32] &= ~bit;
}
}Understanding how the y value turns into a panel, Dn, and An number is perhaps best shown with an example. The following shows how incrementing the y coordinate maps onto panels.
y=b7654 3210 A D P1
0000 0000 0 1
0000 0001 1 1
0000 0010 2 1
0000 0011 3 1
0000 0100 0 1
0000 0101 1 1
0000 0110 2 1
0000 0111 3 1
0000 1000 0 2 P1
...
0000 1111 3 2
0001 0000 0 1 P2
...
0001 1111 3 2 P2
0010 0000 0 1 P3
...
0010 1111 3 2 P3
0011 0000 0 1 P4
...
0011 1111 3 2 P4
01xx xxxx x x P5-P8
1xxx xxxx x x P9-P15Following the x coordinate mapping is ok once the idea of using the coordinate value to orient to the start of the 48-bit triple of 16 B, 16 G and 16 R values across the two rows is figured out. Then it is a case of calculating the offset within the referenced 48-bit triple as per the comments in the code above.
Set Brightness
There is a special command word that can be used to set the overall brightness of the driver chips. Sending this to all 48 chips on a panel will set the brightness for the whole panel.
Full details can be found in the “dimming the panel” section here: https://wiki.hackhitchin.org.uk/index.php?title=LED_panel_interface and in the MBI5034 datasheet (see “Current Gain Adjustment”).
This is the brightness function:
void setBrightness (uint8_t brightpercent) {
uint8_t bright;
if (brightpercent <= 12) {
bright = 0;
} else if ((brightpercent > 12) && (brightpercent < 100)) {
// Scale 12-100% between 0 and default value
bright = (brightpercent - 12) * 0x2B / 88;
} else {
bright = 0x2B;
}
uint16_t ctrl = 0b0111000101000000 | bright;
OESET; // active LOW
LATCLR;
CLKCLR;
for (int a=0; a<FB_BANKS; a++) {
SETADDR(a);
for (int c=0; c<24; c++) {
for (int b=15; b>=0; b--) {
if ((ctrl & (1<<b)) == 0) {
setDataOutput(0);
} else {
setDataOutput(0xFF);
}
if ((c == 23) && (b < 4)) {
LATSET;
} else {
LATCLR;
}
CLKSET;
CLKCLR;
}
}
}
LATCLR;
}Notes:
- The control word is streamed out MSbit first.
- The LATch is held for the last four bits of the write to the last chip.
- The same pattern is written to all Dn bits at the same time.
- Brightness 0 (b000000) corresponds to 12.5% brightness.
- Brightness 63 (b111111) corresponds to 200% brightness.
- The default is 0x2B (b101011) which corresponds to a “gain” of 1 (see datasheet), so I’ve taken that to be considered “normal” or 100% brightness.
- My code only accepts values from 12% to 100%. I’ve not allowed it to go up to 200%.
There is one added complication though – whilst this is going on, the normal scanning should be paused to ensure it doesn’t interfere.
Strictly speaking, the setBrightness routine should pause briefly too, to ensure any existing scanning is complete, but as the interrupt is higher priority than the running code, it should always complete first anyway.
Code Summary
I’ve ended up with the following routines in my code:
void panelInit (void);
void panelScan (void);
void panelClear (bool on=false);
void setPixel (int x, int y, panelcolour col);
panelcolour getPixel (int x, int y);
void setBrightness (uint8_t brightpercent);To use these requires the following basic setup:
#include <TimerOne.h>
#include "LEDPanel.h"
void setup() {
panelInit();
panelClear();
Timer1.initialize(5000); // 200Hz
Timer1.attachInterrupt(panelScan);
setBrightness (BRIGHTNESS_MIN);
}
#define DEL 5
void loop() {
for (int x=0; x<64; x++) {
for (int y=0; y<64; y++) {
setPixel(x, y, led_blue);
delay(DEL);
setPixel(x, y, led_green);
delay(DEL);
setPixel(x, y, led_red);
delay(DEL);
setPixel(x, y, led_white);
delay(DEL);
}
for (int y=0; y<64; y++) {
setPixel(x, y, led_black);
}
}
}Notes:
- This uses the TimerOne library to generate the panelScan() regular update.
- I’ve configured the scan for 200Hz.
- The loop illuminates each column in turn, from top to bottom.
- This assumes the use of four panels as described above.
Find the full code on GitHub here.
Expanding to Four Panels
When it came to it, wiring up four panels wasn’t going to work with the cables and shield I already had, I would have had to have made some longer ones, so in the end I just soldered up a slightly different arrangement of the Arduino shield.
I’ve also added some screw terminals to allow the Arduino to be powered directly via its 5V pin from the main 5V power supply to the panels. I’ve added a 220uF capacitor to attempt to give some stability to the power as the LEDs turn on and off.
I experimented with some 3D printed connectors but couldn’t get anything that connected snugly enough to keep the panels together, so in the end just nailed four panels to a piece of wood as shown.
I have test code for the following:
- To step down the brightness from full to minimum.
- To illuminate each column of LEDs in turn.
- To draw a basic Mandelbrot set.
- To Conway’s Game of Life.
I wasn’t able to capture the “all pixels on” Mandelbrot without the scan lines becoming apparent. I’m not entirely sure why this would be different compared to when it is building up the set or running the Game of Life, but it does. I’ll have to have a think about that one!
Also, I don’t have enough memory on an Uno for a full dual-buffer grid for the Game of Life, so I’m using the pixel buffer itself as the state of the “live” display and have another buffer to calculate the updated values. To have all this on an Uno, I’ve had to limit the display to 52 rows rather than the full 64. Even that only gives me 62 bytes free!
Update: the latest version of the code includes an option to fade dying cells out using different colours. This is shown in the video at the start of this post.
Other Notes
It took me ages to get this working – almost a week on and off. I tried everything – checking the protocol with a scope; working through the interrupt performance and timing requirements; dropping back to a simple test case; ultimately soldering up that interface board and verifying the power supply. I even went right back to first principles and coded up my own routines that I could fully understand.
But I couldn’t get the panels to work.
In the end I took one panel apart and started poking around with an oscilloscope.
Interestingly there are two HC245 line buffers directly off the interface connection. One appears to produce four OE lines and four CLK signals. The other appears to have the address, data and other signals.
It turns out that my crimped connector had a dodgy link on the LATch wire, so although I could see all the correct signals happening at the Arduino end, the LAT signal was never actually making it to the panel!
That will teach me to check first principles and not assume anything.
Or rather, I suspect it won’t and I’ll continue to have plenty more similar issues as I move forward.
As usual a massive thanks to those who have worked out how to do all of the above, and a big thank you to Limehouse Labs for the large giveaway in the first place!
Kevin
-
-
Arduino SP0256A-AL2 Shield Design
Having spent quite a bit of time with an Arduino hooked up to an SP0256A-AL2 on a solderless breadboard, and now that I’ve got some ideas for how I want to sort out the clock, I thought it time to create a PCB to save the unreliable wiring getting in the way!
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to electronics and microcontrollers, see the Getting Started pages.
The Circuit
This is the basic circuit I’ve been using since Part 1 but I’ve added in an option to support the SI5351 from Part 5 instead of the oscillator, too. A solder jumper selects the clock source – there is no default setting.
There is also a (default closed) solder jumper on the RESET line in case there is a need to separate out the reset of the SP0256A-AL2 from the Arduino.
PCB Design
The PCB design is relatively straight forward. Using an Arduino Uno Shield template, all the components have fitted in quite well.
I had to create a custom symbol and footprint for both the SP0256A-AL2 itself and the SI5351 module I’m using.
The following GPIO pins are in use.
ArduinoSP0256A-AL2D2-D7A1-A6D8/ALDD9SBYGNDVSS, A7, A8, TEST, OSC2+5VVDD, VDI, SE, /SBY_RESET/RESET/RESETSI5351 (Optional)A4SDAA5SCLGNDGND+5VVINAs already mentioned, the link between the two /RESET pins is via a solder jumper and pin header connection which can be broken if required.
I’ve added in additional breakout headers for all GPIO that isn’t connected to the SP0256A-AL2: D0-D1, D10-D13, A0-A3.
I’ve not included A4-A5 as these are connected to the SI5351 (if used).
Closing Thoughts
Hopefully this is a fairly straight forward design with no major surprises.
Kevin
-
Ekranı i2C Karakter LCD olan örnek var da, o ekranlar ile yapılabilecekler çok sınırlı olduğu için istemiyorum.
-
İki tâne Arduino var. Köle olan ekranlıyı sürmek için gerekli pinlerden hâriç bir sürü pin boşta kalıyor. Onlarla da istediğini yaptır, ekranda sonuçları yazdır!
Ben daha yeni öğreniyorum!
Neden her şeyi zorlaştırıyorsunuz?Uzaktan Kumandalı RDS'li FM Radyomu yaparken de hiç örnek yoktu!
Ben yapıp paylaştıktan sonra YouTube'da bir sürü örnek çıktı!
Benim gibi acemiden öğrenip paylaşmaya utanmıyor musunuz?
..devamı yanıtta..
-
Arduino bu yaaa!.. Bir tâne Âllah'ın kulu yapıp da YouTube'a video ve GitHub yada GoogleDrive'a kodunu atmamış!
Bunlar bir tek benim aklıma geliyor olamaz!
Niye yapmıyorsunuz arkadaş?
Ekrana veri yazdırmak eğlenceli bir şey değil mi?
YouTube'da Arduino içerik paylaşan zilyon tâne hesap var, birinin de mi aklına gelmemiş yada ilgisini çekmemiş?
..devamı yanıtta..
-
Lehimle içeriden hâlledilebilir, ama o pinleri ekranın kullanmaması lazım!
Ekran takılı Arduino UNO'nun muhafaza kutusunu delme fikri hoşuma gitmediği için proje kalmıştı!
Fakat şimdi yeni çıkan R4'lerde WiFi ve Bluetooth var!
Sonuçta Arduino UNO'dan bahsediyoruz. Hareketli gif bile oynatamadığı için,
Acaba iki tane Arduino® UNO R4 WiFi'yi Bluetooth ile (mecbûr kalmadıkça WiFi istemiyorum!) hâberleştirip, ekranı sürebilir miyim?
..devamı yanıtta...
-
Arduino candır dedim ve şu benim eski Arduino UNO R3'lerden 2.8 Dokunmatik TFT Ekranlı olan ile diğerini haberleştirip, ekranlı olanı monitör gibi kullanma projemi tamamlayayım istedim.
Fakat neden yarım kaldığını hatırladım!
Ekran takılı olan UNO'da haberleşme için boşta uygun pin kalmıyordu!
..devamı yanıtta..
-
Theremin-Style MIDI Controller Does It With Lasers https://hackaday.com/2025/09/07/theremin-style-midi-controller-does-it-with-lasers/ #opticaltheremin #midicontroller #ArduinoHacks #MusicalHacks #ArduinoUno #tofsensor
-
Theremin-Style MIDI Controller Does It With Lasers - Strictly speaking, a Theremin uses a pair of antennae that act as capacitors in a ... - https://hackaday.com/2025/09/07/theremin-style-midi-controller-does-it-with-lasers/ #opticaltheremin #midicontroller #arduinohacks #musicalhacks #arduinouno #tofsensor
-
2025 One Hertz Challenge: Digital Clock Built With Analog Timer - You can use a microcontroller to build a clock. After all, a clock is just somethi... - https://hackaday.com/2025/08/13/2025-one-hertz-challenge-digital-clock-built-with-analog-timer/ #clockhacks #arduinouno #555timer #clock
-
2025 One Hertz Challenge: Digital Clock Built With Analog Timer https://hackaday.com/2025/08/13/2025-one-hertz-challenge-digital-clock-built-with-analog-timer/ #clockhacks #ArduinoUno #555timer #clock #555
-
QUICK PAID OPPORTUNITY HELPING ANSWER A BASIC ARDUINO QUESTION
My friend is desperately trying to finish making their kids birthday present based on an Arduino project found online.
They're offering $20 USD per 10 minutes ($120/hr!) for someone to get on a quick Jitsi call with them asap and just tell them what wire needs to be attached where.
They're basically on step 2 of building this:
https://www.instructables.com/Rat-Operated-Vehicle/...inspired by @mwl 's https://ratoperatedvehicle.com.
It's using an Arduino Uno compatible board (from an Elegoo Robot Car v4).
Please DM me so I can put you in touch, or forward / boost to help reach someone who can help. :)
#Arduino #Maker #RatCar #PetRats #Electronics #Elegoo #ArduinoUno #FOSS #BoostOK
-
Atari 2600 Controller Shield PCB Revisited – Part 3
Following on from Atari 2600 Controller Shield PCB Revisited – Part 2 someone on Mastodon made the point that the reason they tended to use RC circuits to read paddles “back in the day” was due to the expense of ADCs.
Which triggered a bit of an “oh yeah” moment.
The whole point was not to worry about the analog levels at all, and just measure the time it takes for the pin to read HIGH again.
So this looks back at removing the whole ADC thing with a simple “if (digitalRead(pin))” condition!
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to Arduino, see the Getting Started pages.
The Code
The overarching principles are the same as for Atari 2600 Controller Shield PCB Revisited – Part 2 but instead of all the bespoke code to read the analog to digital converter, I’m relying on the following:
- A digital input pin has a threshold for which the input is considered HIGH.
- We can wait for the input reading to register as HIGH instead of looking for absolute thresholds of an analog value.
- For an ATMega328P the threshold is 0.6 x VCC or around 3V. This is equivalent to just over 610 on a 0 to 1023 scale of an equivalent analog reading.
Taking this into account and using largely the same ideas as before, I can reuse most of the code but with the following timing and threshold values instead:
- Start scaling (the 0 point): 10
- End scaling (the 1023 point): 350
The timer TICK is still 100uS and the “breakout” point is still 1000.
When it comes to reading the digital INPUT, I’m using PORT IO once again for speed and expediency.
for (int i=0; i<4; i++) {
if ((PINC & (1<<i)) == 0) {
// Still not HIGH yet
}
}Here is the complete, now greatly simplified, basic code:
#include <TimerOne.h>
#define RAW_START 10
#define RAW_END 350
#define RAW_BREAK 1000
#define RAW_TICK 100
unsigned padState;
unsigned padCount[4];
unsigned atariValue[4];
void atariAnalogSetup() {
Timer1.initialize(RAW_TICK);
Timer1.attachInterrupt(atariAnalogScan);
padState = 0;
}
void atariAnalogScan (void) {
if (padState == 0) {
DDRC = DDRC | 0x0F; // A0-A3 set to OUTPUT
PORTC = PORTC & ~(0x0F); // A0-A3 set to LOW (0)
padState++;
} else if (padState == 1) {
DDRC = DDRC & ~(0x0F); // A0-A3 set to INPUT
for (int i=0; i<4; i++) {
padCount[i] = 0;
}
padState++;
} else if (padState > RAW_BREAK) {
for (int i=0; i<4; i++) {
atariValue[i] = 1023 - map(constrain(padCount[i],RAW_START,RAW_END),RAW_START,RAW_END,0,1023);
}
padState = 0;
} else {
for (int i=0; i<4; i++) {
if ((PINC & (1<<i)) == 0) {
padCount[i]++;
}
}
padState++;
}
}
int atariAnalogRead (int pin) {
return atariValue[pin-A0];
}
void setup() {
Serial.begin(9600);
atariAnalogSetup();
}
void loop() {
Serial.print(padState);
Serial.print("\t[ ");
for (int i=0; i<4; i++) {
Serial.print(atariAnalogRead(A0+i));
Serial.print("\t");
Serial.print(padCount[i]);
Serial.print("\t][ ");
}
Serial.print("\n");
}Closing Thoughts
Sometimes one really can’t see the “wood for the trees” and this was one of those occasions. I was so took up with thinking about how a modern system might think about a problem without thinking about the original reason for the particular solution.
It makes so much more sense thinking about it in these terms now. All it took was an observation from another, namely:
“So I know the RC timer is the classic way to sense analog paddles but they also didn’t have cheap ADCs back then.”
Many thanks “Chip” for that observation 🙂
Kevin
#arduinoUno #atari #atari2600 #include #potentiometer #TICKs
-
Using a Cheap 5V MIDI Interface at 3V3
I had a query about using one of the common Arduino MIDI Shields with an ESP32 and when I looked into it, this is where I got to.
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to Arduino, see the Getting Started pages.
Parts list
- Arduino Uno MIDI Shield
- ESP32 or other 3V3 microcontroller
- 1x 22KΩ, 1x 33KΩ resistors for MIDI IN
- 1x 10Ω, 1×30Ω resistors for MIDI OUT
- Breadboard and jumper wires
Arduino MIDI Shield
These shields are available very cheaply and designed to plug directly onto an Arduino Uno. They are a variation of an original Olimex open source design (details here) but details of the specifics of this shield seem pretty sparse. But it isn’t too complex to figure out.
Essentially it is a 6N139-based 5V MIDI IN circuit and a non-buffered 5V MIDI OUT and THRU. The boards include a switch to cut off RX from MIDI to allow for sketch uploading. There is often a footprint for a button for RESET which may or may not be populated, and there is a block of breakout connectors for all the Arduino’s IO pins.
I did eventually happen upon an uncredited schematic in a random online cloud service file storage, so I’ve copied it over here.
And mapping this over to the board itself I found this PCB diagram. Note this has a URL for “soliddigi” on it, but that doesn’t seem to exist (or do anything) that I could see.
But whilst on the topic of this board, we can map out the IO pin breakouts for future reference too by simply examining the underside of the board:
From this we can see the following pinouts for the board.
Anyway, back to MIDI…
Converting MIDI IN
The output (to the Arduino) from the MIDI IN circuit of this board is a UART RX connection pulled high to 5V. The easiest way to drop that to 3V3 for use with a 3V3 logic system is to use a resistor divider as shown below.
The MIDI shield will still need to be powered by 5V but the output from the RX pin will now be dropped by the ratio 33K/(22K+33K) which is approximately 0.6. So 5V x 0.6 = 3V.
The choice of resistor for this specific application is relatively arbitrary as it is the ratio that is important, but it should be remembered that this is connecting both the optoisolator’s output (RX) and the microcontrollers IO pin (GP19 above) to GND so there will be limits on the amount of current that can be sourced from the two IO pins and that can be sunk into GND.
Converting MIDI OUT
Converting MIDI OUT isn’t quite so easy unfortunately. If the MIDI interface had included a buffer, like the 74HCT14 I’ve used in the past, then no conversion would be necessary – a 3V3 logic signal can drive that directly and the output would be a 5V signal to the rest of the MIDI circuit.
But it doesn’t – it is a directly connected 5V-expecting MIDI OUT circuit involving two 220Ω resistors. If this is driven from a 3V3 level then it is unlikely that there would be enough current to drive the optoisolator at the other end of the MIDI link.
The basic calculation goes as follows (see my MIDI Connections Cheat Sheet for details of the circuits) – this assumes one 220Ω resistor on the receiving side and two on the sending side as per the MIDI spec, and a typical 1.7V drop across the optoisolator.
For 5V operation, from Ohm’s law: Current = (5 – 1.7) / (220+220+200) = 5mA
For 3V3 operation: Current = (3.3 – 1.7) / (220+220+200) = 2mA
This is why the MIDI spec recommends 10Ω and 30Ω resistors for a 3V3 logic OUT circuit:
For proper 3V3 operation: Current = (3.3 – 1.7) / (220+30+10) = 6mA
Really, the easiest method, assuming the MIDI connectors/hardware are required to be used “as is” would be to include a 74HCT14 (the HCT variant, not the HC version) as a buffer by passing the 3V3 signal through two of the inverters.
But at this point, as this is just a couple of resistors, I’d probably look to remove the two 220Ω resistors in the OUTPUT circuit and attempt to patch in a 10Ω and 30Ω instead with the 30Ω pulling up to 3V3 rather than 5V.
Patching the Shield
Given all the above, I believe it is possible to patch this shield to support 3V3 logic operation by performing the following:
- Remove R3 and R4.
- Cut the track to the RX Pin.
- Add 10Ω between MIDI OUT R3 connector and TX.
- Add 33Ω between MIDI OUT R4 connector and 3V3.
- Add 22KΩ between RX and the centre pin of the ON/OFF switch (check this with a meter though – it was the middle pin for me!).
- Add 33KΩ between RX and GND.
WARNING: All of this is theoretical. I’ve not actually done it myself.
Step 1: Remove R3, R4, cut RX track.
Step 2: Add resistors.
Note: I don’t believe that anything has to be done to the THRU as it is taken directly off the RX side of the 6N139, so that will continue to work (as I understand things) off the 5V circuit directly.
Closing Thoughts
Given the number of off-the-shelf 3V3 MIDI boards now available, I suspect in most cases acquiring one directly would be the better option to the above.
But having said that, these boards are available so cheaply and widely that it is worth considering. Also, there are 3V3 logic level boards in Uno form factor, so having a 3V3 version of the shield would be useful in any case.
But I repeat, other than testing the resistor divider on RX, I’ve not tested any of the modifications to the board itself. Do let me know if you’re brave enough to give it a go 🙂
At some point I might make my own 3V3 Uno format shield PCB…
Kevin
-
This is actually a collection of previous projects with the code tidied up a little and combined to show a single sketch that can be configured for either PWM, an R2R ladder or the MCP4725 I2C DAC.
There isn’t really anything here that hasn’t been talked about before somewhere, but hopefully this can act as a single reference point for a range of direct digital synthesis techniques from now on.
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
These are the key Arduino tutorials for the main concepts used in this project:
- Arduino PWM Sound Output.
- Arduino PWM Output Filter Circuit.
- Arduino R2R Digital Audio – Part 3.
- MCP4725 Digital to Analog Converter.
- Arduino Mozzi Additive Synthesis.
If you are new to Arduino, see the Getting Started pages.
Parts list
- Arduino Uno or Nano.
- Cheap or disposable amplification and speaker.
- Several 10K potentiometers.
- One of:
- Breadboard and jumper wires
The Circuit
There is no specific circuit dedicated to this post, but it works well with the two audio experimenter PCBs listed above, which each contain options for the following:
- PWM audio output on either D3 or D9.
- R2R resistor DAC (in the case of the Uno experimenter PCB).
- MPC4725 I2C DAC.
Alternatively, the main elements for PWM operation can be put together on a solderless breadboard as follows.
The Code
As already mentioned the core elements of the code have largely been met before, but the main sections are described below.
The general theory of direct digital synthesis operation is described fully in Arduino R2R Digital Audio – Part 3 so I won’t go over that again.
This code implements simple additive synthesis by using potentiometers to set the amplitudes for a set of sine wave harmonics. The performance of the Arduino largely limits this to being a maximum of six sine waves to be added up, but that is enough for some simple experimentation.
The code configures the first six harmonics: fundamental (f), f*2, f*3, f*4, f*5, f*6. There are some default ranges that can be used for testing without pots:
#define SC 32
int sine[MAXPOTS] = {SC*2,0,0,0,0,0};
int saw[MAXPOTS] = {SC,SC/2,SC/4,SC/8,SC/16,SC/32};
int squ[MAXPOTS] = {SC,0,SC/2,0,SC/4,0};There is no way to set the fundamental frequency – it is fixed at 440Hz. Making that controllable is left as an exercise for another day! The natural options are MIDI triggering or another pot.
The basic properties of the synthesis code are as follows:
- The sample rate depends on the technique, but in principle it could support 4096Hz, 8192Hz, 16384Hz or 32768Hz.
- It uses a 256 entry, 8-bit wavetable to define the basic sine wave.
- It uses 8.8 fixed point accumulators with the top 8-bits as the index into the wave table.
- It uses a 16-bit sample value which is scaled down as required by the audio output method.
The general pattern used in this code is as follows:
// Audio output specific functions:
dacSetup ()
dacWrite (value)
dacScan ()
// Generic audio functions:
dacPlayer ()
Call dacWrite (last calculated sample value)
For each potentiometer:
Update accumulator for the DDS
Add potval * sinetable[accumulator>>8] to the total
setup ()
Call dacSetup ()
loop ()
Call dacScan ()
Every 10 loops update the potsEach audio output option will implement the three functions dacSetup, dacWrite and dacScan, but not all need to be used. Conditional compilation is used to select between audio output options by defining one of PWM_OUTPUT, DAC_OUTPUT or R2R_OUTPUT.
Here are some notes for each option.
PWM:
- Output scaled to 8-bits for use with PWM.
- PWM is configured to run at 65536Hz.
- The TIMERn_OVF_vect interrupt is used to trigger sample updating via dacPlayer().
- All four sample rates are possible so samples are not written out on every interrupt. For example, for a sample rate of 16384Hz a sample is written out on every fourth interrupt.
- Can support either D9 (Timer 1) or D3 (Timer 2).
- As updates are interrupt driven, dacScan () is empty.
R2R:
- Uses D8-D9, D2-D7 as bits 0 to 7 for the DAC output.
- Output scaled to 8-bits for use.
- PORT I/O is used to write to the data lines.
- Code takes into account the fact that D0/D1 might be in use as the UART.
- Updates are interrupt driven using the TimerOne library, calling dacPlayer() directly.
- As updates are interrupt driven, dacScan () is empty.
MCP4725:
- The I2C address for the DAC is configured by defining MCP4725ADDR. It defaults to 0x60.
- As the DAC can’t be written to from an interrupt routine, the output is set during dacScan() so the loop() has to run as fast as possible.
- The sample rate is set by monitoring the micros() tick (note on an Arduino the resolution is 4uS at best).
- Uses the non-blocking I2C library and fast analog read from Mozzi.
- Uses the MCP4725 fast write mode, which only requires two bytes to be sent to the DAC.
- The maximum sample rate is 8192Hz and even then it runs a little slow (i.e. the 440Hz tone is flat by around a semitone).
General comments:
- There is an optional timing pin that is configured by defining TIMING_TEST. This is toggled in dacPlayer().
- There is an optional fixed set of amplitudes that can be used instead of potentiometers. These are set up in setDefaultAmplitudes() when DAC_TEST is defined.
- The maximum number of pots supported is 6. The code skips using A4/A5 as these map onto I2C if the DAC is used. The number of pots to scan (and hence sine waves to add up) can be reduced by setting NUMPOTS to a number less than MAXPOTS (which is 6).
- If the number of pots is reduced, then the scaling factors used to calculating the totals can be adjusted by changing SC and PSC accordingly. For 6 pots/waves they are set to 32 and 4 respectively. This means that analogReads have a maximum range of 0..31 which is set by
val = analogRead(pot) >> PSC;
Closing Thoughts
This has been interesting to revisit. After all my experiments this is starting to finally make some sense. It has been interesting to contrast the three output methods both in terms of their computational performance and in terms of output waveform quality.
The photo at the start shows the R2R output of the test sine wave. The photo below is the PWM output for a potentiometer-driven saw.
This has also prompted me to revisit my Arduino PWM Output Filter Circuit and finally work out how to properly combine a low-pass filter and potential divider and still get something approximating the filter characteristics I wanted. I feel I understand quite a bit more about what is going on now.
Now if I could just get a bit of a handle on impedance I might actually start to feel like I know a little about what I’d be talking about….
Kevin
https://diyelectromusic.wordpress.com/2024/03/06/arduino-direct-digital-additive-synthesis/
#additiveSynthesis #arduinoNano #arduinoUno #dac #dds #mcp4725 #pwm #r2r