Om POV, persistence of vision. POV eller persistence of vision är ett fenomen som uppstår på näthinnan i ögat där en efterbild ser ut att finnas kvar i ungefär en tjugofemtedels sekund efter att den flyttat sig eller försvunnit. Detta fenomen kan man utnyttja genom att låta en rad med radiellt monterade lysdioder rotera på ett hjul och om hjulet roterar tillräckligt fort kan man skapa en bild eller text genom att tända och släcka dioderna vid vissa vinklar. Vi ville undersöka möjligheterna för att montera en sådan anordning på ett cykelhjul och således få ut en bild eller text på sidan av hjulet. För detta proof of concept -arbetet valde vi en uppsättning med 8 ljusdioder monterade på ett cykelhjul som med styrning av en PIC-processor och en halleffektswitch för att mäta omloppstiden på hjulet. Vår inledande tes var att föregående varvtid skulle vara en tillräckligt god approximation för nästa varv i tillämpningen. Hårdvaran Kretskortets huvudkomponenter är: PIC-processor 8 st ljusdioder halleffektswitch magnet (på gaffeln till cykeln) Dessa komponenter plus några motstånd, klockkrets, kondensatorer och annat smått och gott har åstadkommit ett fungerande POV-hjul. Kretsen monteras med hjälp av eltejp eller annan fästanordning efter behag på cykelhjulet så att halleffektswitchen kommer i närheten av magneten en gång per varv. För att driva kretsen används ett batteripack monterat nära navet i hjulet för att undvika allt för stor mekanisk obalans. Kretsen har även stöd för ICSP vilket gör det smidigt att ändra programvaran och på så sätt uppdatera bilden som visas. I bilaga 3 kan man se hur kretsen är monterad på cykelhjulet. Kopplingsschema för kretsen finns i bilaga 6 och ritning över vårt PCB finns i bilaga 7.
Tillverkning Tillverkningen av kretskortet börjades med att rita upp kopplingschema för att därefter anta den större utmaningen att CADa upp hur kretskortet skulle se ut. Efter CAD och godkänd ritning av handledare, skickades ritningen och kopplingschemat för tillverkning. Etsningen av kretskortet gjordes av handledare eftersom detta krävde erfarenhet av utrustningen som användes. Därefter borrades det hål för komponenter för att slutligen löda fast samtliga komponenter. För att ha något att testa kretsen på användes en framgaffel från en cykel med tillhörande hjul, där framgaffeln svetsades fast på en tung metallklump för att den skulle stå stilla. (se bild bilaga 3) Programmeringen Snabbfakta om programmet: Programmerat i C. Nuvarande varvs hastighet antas vara samma som föregående. Timers används för att veta var på varvet man antas vara. Allting nollställs(=nytt varv) när halleffektswitchen passerar den fast installerade magneten. Ger förskjutning kraftig acceleration/retardation. Ett varv är indelat i 128 delar, d.v.s. man kan kontrollera om en diod ska vara tänd eller släckt i intervall om ca 3. Bilden ligger i programminnet som en switch-case sats, då RAM-minnet (data space) inte räcker till för att lagra så stora variabler. Källkoden återfinns i bilaga 2. Själva main-funktioner ser ut som följande. void main() { IoportInit(); while (1) { Där det enda den gör är att initierar portar och startar en oändlig loop för att hålla programmet levandes. Resten av programmet styrs med hjälp av interrupts. Med följande kod. void interrupt interruptfunc(void) { if (INTF) // Extern interrupt (från halleffecktsswitchen) { //INTF=0; // Reset the external interrupt flag prevcnt=cnt; cnt=0; //move=move+10; //Används för att ha en rullande bild if (move >= 128){
move = 0; INTF=0; //Reset external interrupt. else if (T0IF) { //Timer interrupt. if (prevcnt > 128) { //Måste vara så, annars delar vi med 0 senare i koden. int sektorlength = (int)(prevcnt/128); int sektor = cnt/sektorlength + move; // sektor = 128*cnt/prevCnt; if (sektor>127){ sektor = sektor - 128; diodsoutput(imagefunc(sektor)); cnt++; //count, räknar timerinterrupts. T0IF=0; //Reset timerinterrupt. Där första delen hanterar den externa (från halleffektswitchen) interrupten (INTF). Den delen av funktionen sparar undan föregående varvets hastighet (i timerinterrupts-cyckler) och nollställer det nya varvets räknare. Den andra delen av funktionen hanterar timer interrupts (T0IF). Dessa används för att hålla koll var på varvet vi är och tända rätt dioder vid rätt tillfälle. Första vi gör är att kolla så prevcnt > 128. Detta görs för annars kommer det divideras med noll senare i koden. Och om prevcnt < 128, så har hjulet rört sig med väldigt låg hastighet. Om prevcnt > 128 så räknar vi ut hur många timer-cykler det gick på föregående varv och på så sätt får fram hur långt på varvet vi har kommit på det nuvarande varvet, och tänder eller släcker därefter rätt dioder.
Webbgränssnittet Webbgränssnittet hittas på http://dev.brange.nu/mek_proj/, fram till en bra stund efter inlämnandet av denna rapport. Webbgränssnittet var det första gränssnittet vi utvecklade för att skapa data till vårt POV-hjul. Det är ett väldigt enkelt gränssnitt som bygger på 8*128st kryssboxar, som man antingen kan kryssa i eller inte. Dessa motsvarar om en diod ska vara tänd eller släckt på ett specifikt ställe på hjulet. I webbgränssnittet ritar man upp en bild eller ett ord som man vill överföra till hjulet. Man ritar genom att antingen klicka i kryssboxarna manuellt eller genom att hålla inne SHIFT-tangenten samtidigt som man för musen över en eller flera kryssboxar. Därefter klickar man på generera och sedan genereras en switch-case sats som man sedan kan kopiera in i programmet. Källkoden till webbgränssnittet finns i Bilaga 1. Javaprogram för konvertering av digital bild För att undersöka om det skulle kunna gå att få en större bild att se bra ut på hjulet skrevs ett javaprogram som konverterade informationen i en digital bild med xy-koordinater till radiella koordinater i upplösningen som kortet har. Försöken med detta föll inte så väl ut eftersom upplösningen helt enkelt blev för dålig på hjulet samt att användarvänligheten blev låg på grund av dålig grafisk återkoppling för användaren. Trots det dåliga resultatet tror vi att detta system kan fungera väl med högre upplösning på hjulet. Källkoden till detta finns i bilaga 5.
Framtidsplaner Nedan finns tankar och idéer om saker som man kan göra eller förbättra för att POV-hjulet ska bli bättre. Fler lysdioder radiellt längs med hjulet. Fler rader med lysdioder jämt fördelade över hjulet för att öka uppdateringsfrekvensen Reglering av mätvärdena - för att minska vridnings effekt vid acceleration om upplösningen blir högre, framförallt vinkelupplösningen. Kanske med hjälp av en PID regulator. Seriell till parallell-kretsar för att få plats med fler lysdioder (slippa behöva använda onödigt stor PIC-processor) Använda RGB-dioder för att kunna få flera färger. Hantering av externa minnen för enklare programmering. Rörliga bilder - film Trådlös överföring av bild/film från antingen laptop eller mobiltelefon. Bättre fästanordning av kretsen på cykelhjulet
Bilaga 1 - webbgränssnittets källkod <?php // index.php $width = 1200; //Skärmstorlek (på min laptop) $height = 760; $x = $width/2.0; $y = $height/2.0+150; //Antal dioder på hjulet (= max- min) $diod_min = 8; //Hur långt ut de början, ungefär $diod_max = 16; //Hur långt ut de går, ungefär. $vink = 128; //Antal cirkelsektioner. //Formulär med massa checkboxar som ritas ut i en cirkel. Dom motsvarar diodernas position. echo "<form name=\"matris\" method=\"post\" action=\"index.php\">"; for ($r=$diod_min;$r<$diod_max;$r++) { for ($v=0;$v<$vink;$v++) { $left = $x+20.0*$r*cos($v*2*m_pi/$vink+m_pi/2); $top = $y+20.0*$r*sin($v*2*m_pi/$vink+m_pi/2); echo "<input style=\"position:absolute; left: ".$left."px; top: ".$top."px;\" type='checkbox' name='check_".$r."_".$v."' onmouseover=' if (!this.checked) { if (event.shiftkey) this.checked = true; else { if (event.ctrlkey) this.checked = false; '"; if ($_POST["check_".$r."_".$v]) echo "checked"; echo " >"; echo "<input type='submit' name='gen' value='generera' style=\"position:absolute; left: ".($x- 20)."px; top:".$y.";\">"; echo "</form>"; if (isset($_post['gen'])) { //Om Generera- knappen är tryckt. //Skriv ut värdena från checkboxarna i lämpligt format. echo "<div style=\"position:absolute; top:".(2.3*$y)."px;\">"; echo "char imagefunc(int nbr) {<br>"; echo "switch(nbr) {"; for ($v=0;$v<$vink;$v++) { echo "case ".$v.":<br>"; echo "return 0b"; for ($r=$diod_min;$r < $diod_max;$r++) { if ($_POST["check_".$r."_".$v]) echo "0";
else echo "1"; if ($v < $vink- 1) echo ";<br>"; echo ";"; echo "<br>default:<br> "; echo "<br><br></div>";?> </body> </html>
Bilaga 2 - källkod #define XTAL_FREQ 16MHZ #include "pic.h" #include "delay.h" /* DIOD PLATS_PÅ_PICen interrupt RB0 1 RB3 2 RB2 3 RB1 4 RB4 5 RB5 6 RB6 7 RB7 8 RA0 */ void IoportInit(){ TRISA = 0xfe; TRISB = 0x01; PORTB = 0xfe; PORTA = 0xff; //External interrupt(hall- switch) GIE=1; // Global interrupt enable INTF=0; // Reset the external interrupt flag INTE=1; // Enable external interrupts from RB0 //timer0 interrupt T0IE=1; // Enable timer0 interrupt. (internal) T0IF=0; // Reset timer0 interrupt flag. T0CS=0; // Clock source select = 0 => internal. (ENTE RA4/T0CLK pinnen) PSA=0; //Prescaler Assignment, 0 => till timer0 PS0=0; // Bit0: Prescale 128 PS1=0; // Bit1 - "- PS2=0; // Bit2 - "- int cnt=0; int prevcnt=0; int move=0; char imagefunc(int nbr) { switch(nbr) { case 0:
return 0b10111111; case 1: return 0b10111111; case 2: return 0b00111111; case 3: return 0b01111111; case 4: case 5: case 6: case 7: case 8: return 0b10001111; case 9: return 0b00001111; case 10: return 0b11011110; case 11: return 0b10111111; case 12: return 0b00111111; case 13: return 0b01111111; case 14: return 0b01111110; case 15: return 0b11111110; case 16: return 0b11111110; case 17: return 0b11111110; case 18: case 19: case 20: case 21: return 0b11111110; case 22: case 23: case 24: case 25: case 26: return 0b00011111;
case 27: return 0b00111111; case 28: return 0b00111111; case 29: return 0b00111111; case 30: return 0b00011111; case 31: return 0b00011111; case 32: return 0b01111111; case 33: case 34: case 35: case 36: case 37: case 38: case 39: return 0b10001111; case 40: return 0b01111111; case 41: return 0b01111111; case 42: return 0b11100010; case 43: return 0b01011011; case 44: return 0b00111011; case 45: return 0b00110011; case 46: return 0b10001110; case 47: return 0b11111110; case 48: return 0b11111110; case 49: return 0b01111110; case 50: case 51: case 52: case 53:
return 0b11111110; case 54: return 0b01111111; case 55: return 0b00111111; case 56: case 57: case 58: case 59: case 60: case 61: case 62: case 63: case 64: case 65: case 66: case 67: case 68: case 69: case 70: case 71: case 72: case 73: case 74: return 0b11111110; case 75: case 76: case 77: case 78: return 0b11110110; case 79: return 0b11111010;
case 80: return 0b11111000; case 81: return 0b01111010; case 82: return 0b00000011; case 83: case 84: case 85: return 0b11111110; case 86: case 87: return 0b00011111; case 88: return 0b10011111; case 89: case 90: case 91: case 92: case 93: case 94: case 95: return 0b01111111; case 96: return 0b00011111; case 97: return 0b00011111; case 98: return 0b00111111; case 99: return 0b00111111; case 100: return 0b00111111; case 101: return 0b00011111; case 102: case 103: case 104: case 105: case 106:
return 0b11111110; case 107: case 108: case 109: case 110: return 0b11111110; case 111: return 0b11111110; case 112: return 0b01111110; case 113: return 0b00111110; case 114: return 0b00011111; case 115: return 0b01001111; case 116: return 0b01100111; case 117: return 0b11111110; case 118: case 119: case 120: case 121: case 122: case 123: return 0b01111111; case 124: return 0b01111111; case 125: return 0b00111111; case 126: return 0b10111111; case 127: return 0b10111111; default: void diodsoutput(int bitar) { RB3 = bitar%2; bitar=bitar >> 1;
RB2 = bitar%2; bitar=bitar>> 1; RB1=bitar%2; bitar=bitar>> 1; RB4=bitar%2; bitar=bitar>> 1; RB5=bitar%2; bitar=bitar>> 1; RB6=bitar%2; bitar=bitar>> 1; RB7=bitar%2; bitar=bitar>> 1; RA0=bitar%2; void interrupt interruptfunc(void) { if (INTF) { //INTF=0; // Reset the external interrupt flag prevcnt=cnt; cnt=0; move=move+0; if (move >= 128){ move = 0; INTF=0; else if (T0IF) { // T0IF=0; // Reset timer0 interrupt flag if (prevcnt > 128) { int sektorlength = (int)(prevcnt/128); int sektor = cnt/sektorlength + move; // sektor = 128*cnt/prevCnt; if (sektor>127){ sektor = sektor - 128; diodsoutput(imagefunc(sektor)); cnt++; //count, räknar timerinterrupts. T0IF=0; void main(){ IoportInit(); while (1) {
Bilaga 3
Bilaga 4
Bilaga 5 - Bildkonverteringens källkod import java.io.file; public class bildhanterare { public static void main(string[] args){ Reality reality = new Reality(170, 10, 5, 270); Wheel wheel = new Wheel(8,128); RadialImage radialimage = new RadialImage(wheel, reality); File f = new File(args[0]); Bitmap bitmap = new Bitmap(f); radialimage.drawradialimagefrombitmap(bitmap); System.out.println("char imagefunc(int nbr) { \nswitch(nbr) {"); for(int phi = 0 ; phi < wheel.getnumsectors(); phi++){ System.out.print("case " + phi + ":\nreturn 0b"); for(int r = 0 ; r < wheel.getnumleds(); r++){ if(radialimage.getradialpixel(r, phi)) System.out.print(0); else System.out.print(1); System.out.print(";\n"); System.out.print("default: \nreturn 0b"); for(int i=0; i < wheel.getnumleds(); i++){ System.out.print(1); System.out.print(";\n\n"); public class Reality{ private double mmtowheelhub; //distance from the hub to the middle of the innermost LED private double mmbetweenleds; //distance between the center of the LEDs private double mmledwidth; //the width of an LED private double sensorangle; //in degrees, parallel to x- axis and counter clock wise positive public Reality(){ public Reality(double mmtowheelhub, double mmbetweenleds, double mmledwidth, double sensorangle){ this.mmtowheelhub = mmtowheelhub; this.mmbetweenleds = mmbetweenleds; this.mmledwidth = mmledwidth; this.sensorangle = sensorangle;
public double getmmtowheelhub(){ return mmtowheelhub; public double getmmbetweenleds(){ return mmbetweenleds; public double getmmledwidth(){ return mmledwidth; public double getsensorangle(){ return sensorangle; public void setmmtowheelhub(double mmtowheelhub){ this.mmtowheelhub = mmtowheelhub; public void setmmbetweenleds(double mmbetweenleds){ this.mmbetweenleds = mmbetweenleds; public void setmmledwidth(double mmledwidth){ this.mmledwidth = mmledwidth; public void setsensorangle(double sensorangle){ this.sensorangle = sensorangle; public class Wheel{ private int numleds; private int numsectors; public Wheel(){ public Wheel(int numleds, int numsectors){ this.numleds = numleds; this.numsectors = numsectors; public int getnumleds() { return numleds; public int getnumsectors() { return numsectors;
public void setnumleds(int numleds){ this.numleds=numleds; public void setnumsectors(int numsectors){ this.numsectors=numsectors; import java.awt.point; import java.awt.image.bufferedimage; public class RadialImage{ boolean[][] radialimage; Reality reality; Wheel wheel; public RadialImage(Wheel wheel, Reality reality){ radialimage = new boolean[wheel.getnumleds()][wheel.getnumsectors()]; this.reality = reality; this.wheel = wheel; public void setradialpixel(int r, int phi){ radialimage[r][phi]=true; public void clearradialpixel(int r, int phi){ radialimage[r][phi]=false; public boolean getradialpixel(int r, int phi){ return radialimage[r][phi]; public boolean[][] getradialimage(){ return radialimage; public Point translateradialcoordtocartesian(int r, int phi, int imagewidth, int imageheight){ double ddiameter = (Math.min(imageWidth, imageheight)) / (double)((wheel.getnumleds()- 1)*reality.getMmBetweenLEDs()+reality.getMmToWheelHub()); int radius = (int)((r*reality.getmmbetweenleds()+reality.getmmtowheelhub()) * ddiameter / 2); double startangle = (double)phi/wheel.getnumsectors()*360 + reality.getsensorangle(); double dangle = 1.0/(double)wheel.getNumSectors() * 360.0; double angle = startangle + (dangle/2); angle *= Math.PI/180;
return (new Point((int)(Math.cos(angle)*radius+(imageWidth/2)),(int)((imageHeight/2)- Math.sin(angle)*radius))); public void drawradialimagefrombitmap(bitmap bitmap){ Point p; for (int r=0; r<wheel.getnumleds(); r++) { for (int phi=0; phi<wheel.getnumsectors(); phi++) { p = translateradialcoordtocartesian(r, phi, bitmap.getthebufferedimage().getwidth(), bitmap.getthebufferedimage().getheight()); if ( bitmap.getthebufferedimage().getrgb((int)p.getx(), (int)p.gety()) == - 1) { clearradialpixel(r,phi); else { setradialpixel(r,phi); public BufferedImage getimagerepresentation(int width, int height){ BufferedImage image = new BufferedImage(width, height, 1); Point p; for (int r=0; r<wheel.getnumleds(); r++) { for (int phi=0; phi<wheel.getnumsectors(); phi++) { p = translateradialcoordtocartesian(r,phi,width,height); if (getradialpixel(r,phi)){ image.setrgb((int)p.getx(), (int)p.gety(), 0x00ff00ff); return image; import java.awt.image.bufferedimage; import java.io.file; import java.io.ioexception; import javax.imageio.imageio; public class Bitmap{ BufferedImage thebufferedimage; public Bitmap(){ public Bitmap(File f){ try { thebufferedimage = ImageIO.read(f); catch (IOException e) { // TODO Auto- generated catch block
e.printstacktrace(); public BufferedImage getthebufferedimage(){ return thebufferedimage;
Bilaga 6 Kopplingsschema I kopplingsschemat saknas halleffektswitchen samt ett pull-up motstånd till den.
Bilaga 7 PCB