Jump to content
  • Advertisement
  • entries
    222
  • comments
    606
  • views
    593007

About this blog

Z80 and C#-related shenanigans - now with added electronics.

Entries in this blog

 

Superprobe

I have recently been working on building my own Superprobe. This is a cheap and simple tool based around a single PIC 16F870, a four-digit display and a handful of other parts. Hardware details and software can be found on the Superprobe section of the Mondo Technology website.


As the name suggests, at its simplest the Superprobe can be used as a logic probe, displaying an L, H or - if it touches a point in the circuit that is in a low, high or floating state. What makes it so "super" is that by using the two input buttons you can switch it to a different mode. The supplied software provides seventeen different modes, including a logic pulser, frequency counter, voltmeter, capacitance meter, signal generator and serial ASCII data output.


Measuring a 10uF capacitor.
Having such a wide range of functions for such a modest part count made this a very attractive project to build. Unfortunately, I couldn't find a 16F870 so used the pin-compatible 16F876A instead; porting the code from the older microcontroller mainly involved changing the list p=16f870 directive. I did notice that the probe didn't seem to save its settings when powering down as it should, so I copied the EEPROM reading and writing code from the 16F876A datasheet into the source to replace the existing code which seemed to fix it.


The insides of the Superprobe.
As I couldn't find a suitable low drop-out 5V regulator I opted to use a conventional L7805 regulator. This means that the input voltage has to be at least two volts higher than the output; I normally power circuits from a 7.5V or 9V supply anyway so this isn't too much of a problem. Finding a suitable battery to go inside the case was more of a challenge; there's insufficient room for the typical 9V PP3, sadly. A bit of hunting for "7.5V battery" led me to a suitable battery with a variety of names and a rather high price. Aided by a ruler and the dimensions on the above website it seems that the A175 is exactly the same size as five LR44/AG13 cells stacked on top of eachother (coincidence? I think not). A reputable high street shop noted for the quality of its goods sold a card of forty button cells (including ten AG13 cells), so five of those and a bit of masking tape provided me with a passable imitation. Sparing no expense, the battery holder is constructed from paper clips.

The 16F876A has more program space and SRAM than the 16F870, making developing software in C more viable. Not all of of the original software's features were especially useful to me, and I was likely to want to add new modes myself in the future, so set about reimplementing the functions that I did find handy in C. The result is quite a bit easier to modify; for example, the above photograph demonstrating the measurement of a capacitor shows a value with an SI prefix (10.1u for a 10uF capacitor). All one needs to do to display such a number on the display is call display_print_float(10.1e-6f); - the code does the rest for you. Sadly, this does inflate the size of the code significantly and my current version of the code only squeezes 11 functions into a much larger chip (compared to the 17 on the original).


Measuring a 2.2K? resistor.
One of the new modes is a resistance meter. This works by pulling the probe tip high using a known resistance (5K?, 10K? or 100K?) and combining this with the resistor to be measured between the probe tip and ground to form a voltage divider. The output of the voltage divider is measured, and from that the resistance of the resistor being tested can be determined. The ability to use multiplication, division and floating point arithmetic makes this easy to program in C; much more so than it would have been in assembly, at least!

">
View Superprobe video demonstration on YouTube.
I have recorded a video demonstrating the Superprobe. The code for my variation on the theme can be downloaded here, and can be compiled with the free ("lite") edition of the HI-TECH C compiler.

benryves

benryves

 

Controlling a PG320240H-P9 with a dsPIC33FJ128GP802

In a previous entry I mentioned that I had purchased a PG320240H-P9 graphical LCD. This is a 320x240 white-on-blue pixel display, and it does not have an on-board controller or RAM. To display something on it you need to constantly refresh it with picture data; in this instance, sending four pixels at a time, starting from the top left and working from left to right, top to bottom -- a bit like the scanning pattern of a CRT monitor.


Connecting a circuit to the LCD is made slightly more tricky by its use of a 16-pin 1mm flexible flat cable. To get around this I soldered together an adaptor using a suitable FCC connector, pin strip, piece of stripboard and a fairly excessive quantity of hot melt adhesive. Even more tricky was the lack of a suitable datasheet for the LCD. After some digging I located this one for the PG320240WRM-HNNIS1 -- it's slightly different, but contains timing diagrams and specifications that seem to work with the LCD I bought. One thing I still haven't worked out is the contrast adjustment; a 5K variable resistor between 0V and the relevant pin seems to have had the best results thus far. A helpful webpage, Graphical LCD controller for ST8024+ST8016 based displays, has a plain English description of how to drive the LCD, though as far as I'm aware the M pin should have its logic level toggled every frame, giving you a "glass" frequency of half of the refresh rate, not 200Hz-400Hz. The lack of a proper datasheet makes these things a little complicated!


My first attempt to drive the LCD involved an ATmega644P, a microcontroller with 64KB of flash ROM and 4KB of RAM. The above photo shows it displaying a picture of a cat, which was stored in ROM and output using the following code:

#include
#include
#include

#define LCD_FLM (6)
#define LCD_M (5)
#define LCD_C1 (4)
#define LCD_C2 (3)
#define LCD_D_OFF (2)

#define LCD_CONTROL_PORT (PORTC)
#define LCD_CONTROL_PIN (PINC)
#define LCD_CONTROL_DDR (DDRC)

#define LCD_DATA_PORT (PORTA)
#define LCD_DATA_PIN (PINA)
#define LCD_DATA_DDR (DDRA)

#include "cat.h"

int main(void) {

// Make control pins outputs.
LCD_CONTROL_DDR |= _BV(LCD_FLM) | _BV(LCD_M) | _BV(LCD_C1) | _BV(LCD_C2) | _BV(LCD_D_OFF);

// Make data pins outputs.
LCD_DATA_DDR |= 0b1111;

// Enable the LCD.
LCD_CONTROL_PORT |= _BV(LCD_D_OFF);

for(;;) {

// Toggle the M pin to provide the LCD AC voltage.
LCD_CONTROL_PIN |= _BV(LCD_M);

const uint8_t* picture_ptr = cat_picture;

// Scan 240 rows in the image.
for (uint8_t row = 0; row 240; ++row) {

// Begin the line.
LCD_CONTROL_PIN |= _BV(LCD_C1);
LCD_CONTROL_PIN |= _BV(LCD_C1);

if (row 2) LCD_CONTROL_PIN |= _BV(LCD_FLM);

// Send 40 eight-bit words.
for (uint8_t column = 0; column 40; ++column) {
LCD_DATA_PORT = pgm_read_byte(picture_ptr) >> 4;
LCD_CONTROL_PIN |= _BV(LCD_C2);
LCD_CONTROL_PIN |= _BV(LCD_C2);
LCD_DATA_PORT = pgm_read_byte(picture_ptr);
LCD_CONTROL_PIN |= _BV(LCD_C2);
LCD_CONTROL_PIN |= _BV(LCD_C2);
++picture_ptr;
}
}
}

}

A 320x240 display has 76,800 pixels, and if you store each pixel as a single bit (so eight pixels per byte) you need 9600 bytes to store a complete frame, which clearly won't fit in the 4KB offered by the ATmega644P. Rather than upgrade to an AVR with more memory, I jumped to the dsPIC33FJ128GP802, a 16-bit microcontroller with 16KB of RAM. As well as quadrupling the RAM from the ATmega644P it also doubles the program memory (128KB from 64KB) and speed (40 MIPS from 20 MIPS). When working with AVRs I'd been using a slow home-made serial programmer, and rather than continue with this sorry state of affairs (lack of debugging capabilities is never fun, especially when it takes over a minute to program the microcontroller) I treated myself to a PICkit 3 Debug Express.


The above photo shows the LCD connected to the microcontroller as well as the PICkit 3. The dsPIC33FJ128GP802 requires a voltage supply from 3.0V to 3.6V, not the 5V I am used to, so to power it I have put two IN4001 rectifier diodes in series with the 5V regulator output. Each diode incurs a voltage drop of 0.7V, producing 3.6V for the rest of the circuit. The LCD is powered from the main 5V supply, but it seems happy with the 3.6V logic "high" from the dsPIC.

The LCD is connected to the dsPIC as follows:
FLM to RB15 M to RB14 C1 to RB13 C2 to RB12 /D_OFF to RB11 D0~D3 to RA0~RA3 A 10K resistor is included between /D_OFF and ground. This is very important, as it holds the /D_OFF line low when RB11 is floating (e.g. during reset), forcing the display off -- if the display is powered, but is not being actively refreshed, the LCD panel can become overloaded and damaged.

I have knocked together a simple demo that shows a few different graphics on the LCD. The LCD is constantly refreshed by an interrupt service routine that runs in the background, leaving some CPU time to the user program. As there is only enough RAM for a single frame buffer, animation has to be quite simple to avoid flickering, but I've still managed to include my favourite spinning cube.

">
Click to view the demo video on YouTube
The project can be downloaded here. I'm still getting to grips with the dsPIC series; the code is likely to be pretty awful, and I still have a problem where the dsPIC resets itself every couple of minutes (I'm not really sure if this is a software or hardware issue). Still, it's a start, and I hope that I can use this LCD as the display for my Z80 computer project.

Update: Having seen this post, the chap who originally suggested that I investigate the dsPIC33FJ128GP802 sent me an email with some advice, chiefly about my poor power supply, missing decoupling capacitors and use of an electrolytic capacitor on the VCAP pin. I have since replaced the two rectifier diode affair with a proper 3.3V regulator for the power supply, added a decoupling capacitor across AVDD/AVSS and moved the decoupling capacitor between VDD/VSS closer to the microcontroller. I have also ordered some tantalum capacitors to replace the electrolytic one. A bit of debugging found that the watchdog timer is responsible for the spurious resets; I have disabled it in the code for the time being, which has stopped the resets.

benryves

benryves

 

Adding more stereoscopic modes to Quake II's OpenGL renderer

Quake II's OpenGL renderer supports stereoscopic rendering providing you own a video card that has the requisite hardware and driver support ("quad-buffered" OpenGL - rather than a single front and back buffer you have two front buffers and two back buffers, one for each eye). Not owning such a video card I decided to have a go at adding some other stereoscopic rendering modes that worked with regular hardware.


The four new stereoscopic rendering modes
The ability to enable or disable drawing with a particular colour component in OpenGL makes implementing an anaglyph mode very simple - temporarily switch off red when drawing the view from one eye and temporarily switch off blue and green when drawing the view from the other to produce a final image that can be used with red/cyan 3D glasses. A new string console variable cl_stereo_anaglyph_colors can be amended to set the colour of your particular glasses, e.g. mg for magenta/green.

By drawing a mask to the stencil buffer before rendering one can easily add "interleaved" modes; there's the standard row interleaved format, but I've also added column and pixel interleaved formats.

It looks like the stereoscopic OpenGL code was started but not finished in Quake II; there were a number of odd bugs, such as the viewport position being changed instead of the camera position when drawing the left and right eye views (producing two views that were offset in 2D, not 3D). A snippet of code hints towards why this may be:

#if 0 // commented out until H3D pays us the money they owe us
GL_DrawStereoPattern();
#endif

H3D manufactured a VGA adaptor for 3D glasses which relied on a special pattern being displayed on the screen to enable it and switch it to the correct mode rather than let the user do so. H3D went bust toward the end of 1998, so I guess id software never got their money and that function has been commented out ever since.


The new binaries can be downloaded from the Stereo Quake page, and the source can be found on Google Code.

benryves

benryves

 

ATmega644P CHIP-8/SCHIP interpreter

In an attempt to solve the screen resolution problem issue I've bought a very cheap 320x240 pixel graphical LCD - a PG320240H-P9 on eBay for $24. Part of the reason for its cheapness may be down to its the lack of a controller; you need to constantly refresh the LCD with pixel data yourself (easier to use modules have integrated controllers that refresh the display for you from some on-board RAM). If I manage to get it working I'll have a 128x64 pixel graphical LCD going spare - finding a use for it could make an interesting project.

I have a bit of a soft spot for the CHIP-8 programming language, having previously written an few implementations. The CHIP-8 environment requires just under 3.5KB of RAM, and my recent investment in an ATmega644P boasting 4KB of RAM provided me with a microcontroller that was up to the task.


Beyond the ATmega644P and LCD the hardware is pretty simple; a potentiometer is provided to adjust the speed of the interpreter when it's running, from 1/8th speed up to 8x speed. Sound is output using a piezo transducer, which I've taped to the hard plastic lid from a tube of chocolates to amplify it. Games rely on a 4x4 hex keypad for input, and as I do not have a 4x4 keypad - hex or otherwise - I assembled my own on another breadboard. I don't even have sixteen switches of the same type, hence the mixture in the above photo. A schematic of the hardware can be downloaded in PDF format.


When you reset the circuit a list of all of the programs stored on the microcontroller is shown on the LCD. The 64KB of flash memory on the ATmega644P is enough to store the code for the interpreter and all of the CHIP-8 and SCHIP games available on the Internet. For a change I've decided to have a go at designing a variable width font rather than use one of my existing fixed-width fonts; I don't think it looks too shabby.


When a game has been selected a (gramatically incorrect) summary of the game is shown. To the right of the screen is a 4x4 grid informing the player which key does what; arrows for directional controls, a diamond for "fire" or confirmation actions and a tick/cross for yes/no input. There doesn't seem to be any particular convention for keypad input in CHIP-8/SCHIP games, which makes this feature invaluable!


Click here to download the source code.

benryves

benryves

 

Thinking about CP/M

It's been some time since I worked on my Z80 computer project, but the recent electronics projects I've completed have got me thinking about it again.

">
Click to watch the video on YouTube
I did record a video to demonstrate the basic parts of the computer and some of its flaws a few months ago, which can be seen above. However, I'm now thinking of a more radical redesign than fixing the I/O board's shortcomings.

One of the reasons for my lack of motivation is that even if I did get something working I wouldn't have much software to run on it; it would be a lot of work to write software that only ran on that one particular machine. BBC BASIC helps somewhat, but an even better solution would be to model the device on an existing machine and run its operating system on it.

Fortunately, there was a popular operating system for the 8080 (and, by extension, the Z80) - CP/M. This is a very simple operating system that inspired DOS. Crucially, it is not hardware-specific, the source code is available and there is a wide range of software available for it, including BBC BASIC.

CP/M is made up of three main components. At the highest level is the Console Command Processor, or CCP. This provides the command-line interface, a handful of built-in commands and handles loading and executing external programs. It achieves this with the aid of the Basic Disk Operating System, or BDOS, which exposes a number of useful routines for a variety of tasks, such as outputting text to the display, searching for files on the disk or reading console input.

Both of the above components are machine-independent - they simply need to be copied to the correct address in RAM when the computer starts. Relocating them to a particular address requires setting a single value in their respective source files and reassembling them, which is nice and easy. It's the third component - the Basic I/O System, or BIOS - that requires a bit more work. This is the only part that is tailored to a particular machine's hardware, and my current implementation is listed below.

CCP = $DC00
BDOS = $E406
BIOS = $F200

IOBYTE = $0003
CDISK = $0004

DMAAD = $0008
CTRACK = $000A
CSEC = $000C

.org BIOS

jp BOOT ; COLD START
WBOOTE
jp WBOOT ; WARM START
jp CONST ; CONSOLE STATUS
jp CONIN ; CONSOLE CHARACTER IN
jp CONOUT ; CONSOLE CHARACTER OUT
jp LIST ; LIST CHARACTER OUT
jp PUNCH ; PUNCH CHARACTER OUT
jp READER ; READER CHARACTER OUT
jp HOME ; MOVE HEAD TO HOME POSITION
jp SELDSK ; SELECT DISK
jp SETTRK ; SET TRACK NUMBER
jp SETSEC ; SET SECTOR NUMBER
jp SETDMA ; SET DMA ADDRESS
jp READ ; READ DISK
jp WRITE ; WRITE DISK
jp LISTST ; RETURN LIST STATUS
jp SECTRAN ; SECTOR TRANSLATE

DISKPARAM
.dw $0000 ; No sector translation.
.dw $0000 ; Scratch
.dw $0000 ; Scratch
.dw $0000 ; Scratch
.dw DIRBUF ; Address of a 128-byte scratch pad area for directory operations within BDOS. All DPHs address the same scratch pad area.
.dw DPBLK ; Address of a disk parameter block for this drive. Drives with identical disk characteristics address the same disk parameter block.
.dw CHK00 ; Address of a scratch pad area used for software check for changed disks. This address is different for each DPH.
.dw ALL00 ; Address of a scratch pad area used by the BDOS to keep disk storage allocation information. This address is different for each DPH.

DIRBUF
.fill 128

DPBLK ; DISK PARAMETER BLOCK, COMMON TO ALL DISKS
.DW 26 ; SECTORS PER TRACK
.DB 3 ; BLOCK SHIFT FACTOR
.DB 7 ; BLOCK MASK
.DB 0 ; NULL MASK
.DW 242 ; DISK SIZE-1
.DW 63 ; DIRECTORY MAX
.DB 192 ; ALLOC 0
.DB 0 ; ALLOC 1
.DW 16 ; CHECK SIZE
.DW 2 ; TRACK OFFSET

CHK00
.fill 16

ALL00
.fill 31

; =========================================================================== ;
; BOOT ;
; =========================================================================== ;
; The BOOT entry point gets control from the cold start loader and is ;
; responsible for basic system initialization, including sending a sign-on ;
; message, which can be omitted in the first version. ;
; If the IOBYTE function is implemented, it must be set at this point. ;
; The various system parameters that are set by the WBOOT entry point must be ;
; initialized, and control is transferred to the CCP at 3400 + b for further ;
; processing. Note that register C must be set to zero to select drive A. ;
; =========================================================================== ;
BOOT
xor a
ld (IOBYTE),a
ld (CDISK),a
jp GOCPM

; =========================================================================== ;
; WBOOT ;
; =========================================================================== ;
; The WBOOT entry point gets control when a warm start occurs. ;
; A warm start is performed whenever a user program branches to location ;
; 0000H, or when the CPU is reset from the front panel. The CP/M system must ;
; be loaded from the first two tracks of drive A up to, but not including, ;
; the BIOS, or CBIOS, if the user has completed the patch. System parameters ;
; must be initialized as follows: ;
; ;
; location 0,1,2 ;
; Set to JMP WBOOT for warm starts (000H: JMP 4A03H + b) ;
; ;
; location 3 ;
; Set initial value of IOBYTE, if implemented in the CBIOS ;
; ;
; location 4 ;
; High nibble = current user number, low nibble = current drive ;
; ;
; location 5,6,7 ;
; Set to JMP BDOS, which is the primary entry point to CP/M for transient ;
; programs. (0005H: JMP 3C06H + b) ;
; ;
; Refer to Section 6.9 for complete details of page zero use. Upon completion ;
; of the initialization, the WBOOT program must branch to the CCP at 3400H+b ;
; to restart the system. ;
; Upon entry to the CCP, register C is set to thedrive;to select after system ;
; initialization. The WBOOT routine should read location 4 in memory, verify ;
; that is a legal drive, and pass it to the CCP in register C. ;
; =========================================================================== ;
WBOOT

GOCPM
ld a,$C3 ; C3 IS A JMP INSTRUCTION
ld ($0000),a ; FOR JMP TO WBOOT
ld hl,WBOOTE ; WBOOT ENTRY POINT
ld ($0001),hl ; SET ADDRESS FIELD FOR JMP AT 0

ld ($0005),a ; FOR JMP TO BDOS
ld hl,BDOS ; BDOS ENTRY POINT
ld ($0006),hl ; ADDRESS FIELD OF JUMP AT 5 TO BDOS

ld bc,$0080 ; DEFAULT DMA ADDRESS IS 80H
call SETDMA

ei ; ENABLE THE INTERRUPT SYSTEM
ld a,(CDISK) ; GET CURRENT DISK NUMBER
ld c,a ; SEND TO THE CCP
jp CCP ; GO TO CP/M FOR FURTHER PROCESSING

; =========================================================================== ;
; CONST ;
; =========================================================================== ;
; You should sample the status of the currently assigned console device and ;
; return 0FFH in register A if a character is ready to read and 00H in ;
; register A if no console characters are ready. ;
; =========================================================================== ;
CONST
out (2),a \ ret

; =========================================================================== ;
; CONIN ;
; =========================================================================== ;
; The next console character is read into register A, and the parity bit is ;
; set, high-order bit, to zero. If no console character is ready, wait until ;
; a character is typed before returning. ;
; =========================================================================== ;
CONIN
out (3),a \ ret

; =========================================================================== ;
; CONOUT ;
; =========================================================================== ;
; The character is sent from register C to the console output device. ;
; The character is in ASCII, with high-order parity bit set to zero. You ;
; might want to include a time-out on a line-feed or carriage return, if the ;
; console device requires some time interval at the end of the line (such as ;
; a TI Silent 700 terminal). You can filter out control characters that cause ;
; the console device to react in a strange way (CTRL-Z causes the Lear- ;
; Siegler terminal to clear the screen, for example). ;
; =========================================================================== ;
CONOUT
out (4),a \ ret

; =========================================================================== ;
; LIST ;
; =========================================================================== ;
; The character is sent from register C to the currently assigned listing ;
; device. The character is in ASCII with zero parity bit. ;
; =========================================================================== ;
LIST
out (5),a \ ret

; =========================================================================== ;
; PUNCH ;
; =========================================================================== ;
; The character is sent from register C to the currently assigned punch ;
; device. The character is in ASCII with zero parity. ;
; =========================================================================== ;
PUNCH
out (6),a \ ret

; =========================================================================== ;
; READER ;
; =========================================================================== ;
; The next character is read from the currently assigned reader device into ;
; register A with zero parity (high-order bit must be zero); an end-of-file ;
; condition is reported by returning an ASCII CTRL-Z(1AH). ;
; =========================================================================== ;
READER
out (7),a \ ret

; =========================================================================== ;
; HOME ;
; =========================================================================== ;
; The disk head of the currently selected disk (initially disk A) is moved to ;
; the track 00 position. If the controller allows access to the track 0 flag ;
; from the drive, the head is stepped until the track 0 flag is detected. If ;
; the controller does not support this feature, the HOME call is translated ;
; into a call to SETTRK with a parameter of 0. ;
; =========================================================================== ;
HOME
ld bc,0
jp SETTRK

; =========================================================================== ;
; SELDSK ;
; =========================================================================== ;
; The disk drive given by register C is selected for further operations, ;
; where register C contains 0 for drive A, 1 for drive B, and so on up to 15 ;
; for drive P (the standard CP/M distribution version supports four drives). ;
; On each disk select, SELDSK must return in HL the base address of a 16-byte ;
; area, called the Disk Parameter Header, described in Section 6.10. ;
; For standard floppy disk drives, the contents of the header and associated ;
; tables do not change; thus, the program segment included in the sample ;
; CBIOS performs this operation automatically. ;
; ;
; If there is an attempt to select a nonexistent drive, SELDSK returns ;
; HL = 0000H as an error indicator. Although SELDSK must return the header ;
; address on each call, it is advisable to postpone the physical disk select ;
; operation until an I/O function (seek, read, or write) is actually ;
; performed, because disk selects often occur without ultimately performing ;
; any disk I/O, and many controllers unload the head of the current disk ;
; before selecting the new drive. This causes an excessive amount of noise ;
; and disk wear. The least significant bit of register E is zero if this is ;
; the first occurrence of the drive select since the last cold or warm start. ;
; =========================================================================== ;
SELDSK
ld hl,DISKPARAM
ld a,c
or a
ret z
ld hl,$0000 ; Only disc 0 is supported.
ret

; =========================================================================== ;
; SETTRK ;
; =========================================================================== ;
; Register BC contains the track number for subsequent disk accesses on the ;
; currently selected drive. The sector number in BC is the same as the number ;
; returned from the SECTRAN entry point. You can choose to seek the selected ;
; track at this time or delay the seek until the next read or write actually ;
; occurs. Register BC can take on values in the range 0-76 corresponding to ;
; valid track numbers for standard floppy disk drives and 0-65535 for ;
; nonstandard disk subsystems. ;
; =========================================================================== ;
SETTRK
ld (CTRACK),bc
ret

; =========================================================================== ;
; SETSEC ;
; =========================================================================== ;
; Register BC contains the sector number, 1 through 26, for subsequent disk ;
; accesses on the currently selected drive. The sector number in BC is the ;
; same as the number returned from the SECTRAN entry point. You can choose to ;
; send this information to the controller at this point or delay sector ;
; selection until a read or write operation occurs. ;
; =========================================================================== ;
SETSEC
ld (CSEC),bc
ret

; =========================================================================== ;
; SETDMA ;
; =========================================================================== ;
; Register BC contains the DMA (Disk Memory Access) address for subsequent ;
; read or write operations. For example, if B = 00H and C = 80H when SETDMA ;
; is called, all subsequent read operations read their data into 80H through ;
; 0FFH and all subsequent write operations get their data from 80H through ;
; 0FFH, until the next call to SETDMA occurs. The initial DMA address is ;
; assumed to be 80H. The controller need not actually support Direct Memory ;
; Access. If, for example, all data transfers are through I/O ports, the ;
; CBIOS that is constructed uses the 128 byte area starting at the selected ;
; DMA address for the memory buffer during the subsequent read or write ;
; operations. ;
; =========================================================================== ;
SETDMA
ld (DMAAD),bc
ret

; =========================================================================== ;
; READ ;
; =========================================================================== ;
; Assuming the drive has been selected, the track has been set, and the DMA ;
; address has been specified, the READ subroutine attempts to read one sector ;
; based upon these parameters and returns the following error codes in ;
; register A: ;
; ;
; 0 - no errors occurred ;
; 1 - nonrecoverable error condition occurred ;
; ;
; Currently, CP/M responds only to a zero or nonzero value as the return ;
; code. That is, if the value in register A is 0, CP/M assumes that the disk ;
; operation was completed properly. If an error occurs the CBIOS should ;
; attempt at least 10 retries to see if the error is recoverable. When an ;
; error is reported the BDOS prints the message BDOS ERR ON x: BAD SECTOR. ;
; The operator then has the option of pressing a carriage return to ignore ;
; the error, or CTRL-C to abort. ;
; =========================================================================== ;
READ
out (13),a \ ret

; =========================================================================== ;
; WRITE ;
; =========================================================================== ;
; Data is written from the currently selected DMA address to the currently ;
; selected drive, track, and sector. For floppy disks, the data should be ;
; marked as nondeleted data to maintain compatibility with other CP/M ;
; systems. The error codes given in the READ command are returned in register ;
; A, with error recovery attempts as described above. ;
; =========================================================================== ;
WRITE
out (14),a \ ret

; =========================================================================== ;
; LISTST ;
; =========================================================================== ;
; You return the ready status of the list device used by the DESPOOL program ;
; to improve console response during its operation. The value 00 is returned ;
; in A if the list device is not ready to accept a character and 0FFH if a ;
; character can be sent to the printer. A 00 value should be returned if LIST ;
; status is not implemented. ;
; =========================================================================== ;
LISTST
out (15),a \ ret

; =========================================================================== ;
; SECTRAN ;
; =========================================================================== ;
; Logical-to-physical sector translation is performed to improve the overall ;
; response of CP/M. Standard CP/M systems are shipped with a skew factor of ;
; 6, where six physical sectors are skipped between each logical read ;
; operation. This skew factor allows enough time between sectors for most ;
; programs to load their buffers without missing the next sector. In ;
; particular computer systems that use fast processors, memory, and disk ;
; subsystems, the skew factor might be changed to improve overall response. ;
; However, the user should maintain a single-density IBM-compatible version ;
; of CP/M for information transfer into and out of the computer system, using ;
; a skew factor of 6. ;
; ;
; In general, SECTRAN receives a logical sector number relative to zero in BC ;
; and a translate table address in DE. The sector number is used as an index ;
; into the translate table, with the resulting physical sector number in HL. ;
; For standard systems, the table and indexing code is provided in the CBIOS ;
; and need not be changed. ;
; =========================================================================== ;
SECTRAN
ld h,b
ld l,c
ret

Quite a number of the above routines simply output the value of the accumulator to a port. This is because I'm running CP/M in a Z80 emulator that I've knocked together, and am handling writes to particular ports by implementing the machine-specific operations (such as console input or output) in C#. The floppy disk file system is also emulated in C#; when the program starts, it pulls all the files from a specified directory into an in-memory disk image. Writing to any sector deletes all of the files in this directory then extracts the files from the in-memory virtual disk image back into it. This is not especially efficient, but it works rather well.


Click to view at full size
To turn this into a working bit of hardware, I intend to replace the C# part with a microcontroller to handle keyboard input, text output and interfacing to an SD card for file storage. It would also be responsible for booting the system by copying the OS to Z80 memory from the SD card. I'm not sure the best way to connect the microcontroller to the Z80, though; disk operations use DMA, which is easy enough, but for lighter tasks such as querying whether console input is available or outputting a character to the display it would be nice to be able to go via I/O ports. A couple of I/O registers may be sufficient as per the current design; a proper Z80 PIO would be even better if I can get my hands on one.

Of more concern is a suitable display; the above screenshot is from an 80-character wide display. Assuming a character was four pixels wide (which is about as narrow as they can be made whilst still being legible) imposes a minimum resolution of 320 pixels horizontally - my current LCD is only 128 pixels wide (not even half way there), and larger ones are really rather expensive!

benryves

benryves

 

Building a VGA line blanker and 3D glasses driver

Assembling a circuit on breadboard is a good way to experiment with electronics, but the result is not something you could really use - it's bulky, fragile and awkward to set up. It's far nicer to solder the components of the circuit together to form a more permanent device and put it in a enclosure to make it robust. This is not something I'm especially good at, but something I thought I'd try with the VGA line blanker and LCD shutter glasses controller I've been experimenting with recently.


In the past I've struggled along with a hand drill and the nail file on a Swiss Army knife, but have more recently acquired a high-speed rotary tool and an assortment of attachments which make things much easier. I took some photos when building this project, which I've documented below; I'm not sure my techniques are very efficient, but I do get there in the end. I'd be very glad to hear any advice anyone has!


I started with a plain project box. Having planned roughly where I was going to put the VGA ports and DC power socket, I covered one side of the box in masking tape and drew on where I was going to put the holes.


To cut straight-edged holes, such as those required for a D-subminiature connector, I drill a hole in each corner and use a small cylindrical burr to cut between the holes. This leaves a very rough edge, but is a good start.


I then widen the hole using a large cylindrical burr and a needle file until the part I'm attempting to mount fits snugly.


When I had both VGA connectors in place, I marked and drilled the holes for the jack posts that the VGA leads will screw into. Neither hole is especially neatly cut, but the D-subminiature connector overlaps the hole sufficiently to hide any shoddy workmanship.


The last part of the back is the DC power socket. As I don't have a drill bit large enough to cut the hole on its own, I drill it as large as I can then widen it using the cylindrical burrs mentioned before. With all of the holes cut, I inserted the components to see how they look and identified one problem - I'd underestimated how fat the connectors on the end of VGA leads are. Fortunately, I have a slim VGA cable that fits, but a regular sized one does not - in future I'll need to remember to put the VGA connectors further apart!


With that mistake fresh in my mind, I thought I'd move onto something a bit more difficult to get wrong - the 3.5mm stereo jack on the front of the box to plug the glasses into. This is just another round hole, cut in the same way as the DC power socket.


The two control switches on the top of the box require much larger holes. These were cut in the same way as before - a small hole is gradually widened by using a cylindrical burr. This is a very tedious job, not helped by having to keep stopping to clean the melted plastic that adheres to the burr.


Finally, the switches were installed. I was originally going to use latching push buttons, but had previously used those nice round rocker switches as the power switch on the AVR TV Game project so opted to use them instead.


The final bit of physical work was to cut some stripboard down to size to fit inside the enclosure. These were cut by first scoring along the tracks where the cut was to be made, then snapping the board over the edge of a table. This results in a clean break, but to ensure a snug fit the boards were tidied up with a sanding drum. The lid (or, in my case, base) of the enclosure has a raised edge that fits inside the box, so the sanding drum was also used to remove two of the corners of the stripboard pieces to allow the base to fit.


The next stage was to move onto the electronics, and I started with the circuit board that was to host the video amplifier IC, voltage regulator and Schmitt trigger on vsync/hsync. The video amplifier is attached to a TSSOP14 adaptor that has a D-shaped pin configuration, with two rows of four pins and two rows of three pins. Having cut through the tracks in the stripboard to mount the amplifier, I needed to find some suitable pin sockets.


As I don't have any pin sockets with just three pins in them (only two, four and eight) I cut two eight-way pin sockets in two with a pair of wire cutters then tidied up the ragged edges with a sanding drum and needle file.


With the pin sockets soldered in place you can see the D shape I mentioned above. I don't generally plan stripboard circuits very thoroughly, preferring to start by placing large components in approximately the right location with respect to where the external connectors are and how they need to relate to other components. Once those are in place I add smaller components (such as discrete resistors or capacitors) before finishing by adding the wire links to connect all of the parts together. This does lead to situations where I wish that I'd placed a component one hole along to give myself more space or to avoid having to insert so many wire links, but it generally works.


With the video amplifier in position, I added the resistors that are required on its inputs and outputs. To keep the circuit reasonably compact I cut through stripboard tracks between the holes using a conical HSS burr with a small tip - this is an especially useful tool when you need to deal with double-row pin sockets


I then added the support circuitry for the voltage regulator (smoothing capacitors and a rectifier diode to protect the circuit if the polarity of the power supply is incorrect) and a socket for the Schmitt trigger IC. I find the easiest way to keep components in place on any sort of through-hole board is to tape them down firmly with masking tape before soldering - bending the legs out makes the parts much harder to remove if you make a mistake. Blu-Tack is easier to use but has a habit of melting when soldering and leaving an unpleasant blue residue on your circuit, so I'd advise against it! To make this part of the circuit slightly more future-proof a pair of jumpers are used to connect the sync lines (vsync and hsync) from the VGA input and VGA output together. These could be removed if I decided to change the logic board to override these signals - for example, as part of a sync-doubler, which injects a vsync pulse half way down the screen.


I finally added the bulkiest components; the 5V regulator and the pin header to connect the upper and lower boards together. Soldering pin headers to the underside of a board is a fiddly job, but is required in this instance to connect the bottom of the upper board to the top of the lower board.


With the upper board completed it was time to put it into the enclosure and solder the VGA connectors and DC power socket to it. This is the part I least enjoy.


I started by soldering some stranded wire to the VGA connectors. Most of the wires are the same length, as they are required to carry signals to and from the circuit, but some wires are shorter and only connected to one of the VGA connectors. These are the white, yellow, orange and brown wires in the above photo, and these are attached to pins used to exchange information between the PC and the monitor (e.g. supported resolutions and refresh rates). As we're not interested in these, they're connected straight through from one connector to the other.


I inserted the VGA connector with these identification pins into the top hole, passed the shorter identification wires through the other and soldered them to the second VGA connector. This leaves the red, green, blue, vsync, hsync and ground pins loose inside, ready to be connected to the upper circuit board.


The DC power socket also needs to be connected to the circuit board, but at only two wires that's a much simpler job.


All of the loose leads are soldered onto the circuit board and stripboard is slotted into place inside the enclosure. The wires could be shorter, but that would have made soldering them a bit harder.


The lower circuit board will host the main logic for the project - it receives the vsync and hsync signals, and uses these to control whether the video signal should be blanked or not, and which shutter on the glasses should be closed and which should be open. It also contains the oscillator that generates the AC voltage that drives the glasses. I arranged the three logic ICs roughly next to eachother according to their layout on the breadboard version of the circuit and cut the stripboard tracks as appropriate.


I started by adding the sockets for the ICs and pin header to connect this circuit board to the video amplifier one, then added the discrete components. As before, I taped the components down before soldering them in place to make the task easier. Being able to copy the circuit directly from the breadboard version also made the task much easier.


The last step for this part of the project was, as before, adding the wire links. Rather than run long wires around ICs I found it more practical to solder a few wires onto the underside of the stripboard.


The two circuit boards needed to be connected together somehow. Without the facilities to make a proper ribbon cable, I just soldered some lengths of stranded wire (rather messily) between two pin sockets. As I'm not outputting anything to vsync or hsync (I'm feeding the input sync signals straight back to the output via the jumpers previously discussed), I didn't need to connect anything to these pins - hence the apparently missing wires in the photos.


The cable to connect the two boards together needed to be bent to fit - it's getting snug, but everything's in there without having to be forced, which is a good sign.


The next job was to attach the 3.5mm stereo jack that the LCD shutter glasses are plugged into. This is pushed through the hole in the enclosure from the inside and screwed on from the outside, so it can be soldered directly to the circuit board without having to thread it through the hole first. The small red "washer" is a length of enamelled wire that has been bent around the thread of the jack socket and is used as a spacer - without it, quite a lot of the thread protrudes from the front of the box, looking rather untidy.


Last of all are the two control switches. These are soldered to the track side of the stripboard like the stereo jack, but must be snapped through their holes in the enclosure first, which is why they were left until last. Everything is slotted into place, the base of the enclosure is screwed on, and the project is pretty much complete.


The VGA cables don't fit especially well - the D-subminiature sockets are a bit too close to eachother. If I use a thin VGA extension cable and wiggle the leads I can just about get both to screw in.


The demonstration pattern from some previous ramblings of mine is quite useful for testing 3D glasses, and by holding the left eye of the shutter glasses to the screen you can see that only the "L" part of the image is let through.

benryves

benryves

 

Adding a stereoscopic renderer to Quake II

Having tweaked the stereoscopic rendering code in Quake, I decided to have a go at Quake II. This doesn't natively support row-interleaved stereoscopic rendering, but I thought that the shared code base of Quake and Quake II should make extending Quake II relatively simple.

Quake II does have two console variables dedicated to stereoscopic rendering already, cl_stereo (enable/disable stereoscopic rendering) and cl_stereo_separation (controls the displacement of the camera between eyes; the same as LCD_X in Quake). These variables only seem to be used in the OpenGL renderer, though I haven't been able to get them to do anything meaningful - I have a hunch that you need a video card that supports stereoscopic rendering; these do exist, and have a socket on them for 3D glasses, but I'm having to make do with my DIY hardware. Furthermore, I've always found the OpenGL rendering in Quake and Quake II incredibly ugly, with blurry low-resolution textures (this is the reason I opted to emulate the software renderer when writing my own implementation of the Quake engine).


It turns out that Quake II does indeed render each frame twice with the camera offset when cl_stereo is switched on, but the software renderer doesn't do anything to blend the two views together. Using the same tricks as Quake - halving the height of the viewport, doubling the apparent stride of the render surface, shunting the address of the buffer down one scanline for one eye - seems to have done the trick, though finding out when exactly to carry out these steps hasn't been all that smooth. The particle rendering code still crashes with an access violation if called twice during a frame, but only in release mode. Fortunately, the entire software renderer has been written in C and assembly, so I've reverted to the C-based particle renderer instead of the assembly one for the time being as that doesn't appear to be affected by the same bug.

A slightly more bothersome problem is the use of 8-bit DirectDraw modes for full-screen rendering. Unfortunately, Windows seems to like interfering with the palette resulting in rather hideous colours. Typing vid_restart a few times into the console may eventually fix the issue, but it's far from an ideal solution. An alternative may be to rewrite the code to output 32-bit colour; this would also allow for coloured lighting. Unfortunately, I don't think I'd be especially good at rewriting the reams of x86 assembly required to implement such a fix, and the C software renderer I previously mentioned results in a slightly choppy framerate at high resolutions.

An alternative would be to learn how to use Direct3D from C and rewrite the renderer entirely, taking advantage of hardware acceleration but this would seem like an equally daunting task. If anyone has any suggestions or recommendations I'd be interested to hear them!


Replacement binaries for Quake and Quake II can be downloaded from the project page; source code is available on Google Code.

benryves

benryves

 

3D glasses, a VGA line-blanker and fixing Quake

Some time ago, I posted about using interlaced video to display 3D images. Whilst the idea works very nicely in theory, it's quite tricky to get modern video cards to generate interlaced video at a variety of resolutions and refresh rates. My card limits me to 1920x1080 at i30 or 1920x1080 at i25, and only lets me use this mode on my LCD when I really need it on a CRT. Even if you can coax the video card to switch to a particular mode, this is quite a fragile state of affairs as full-screen games will switch to a different (and likely progressively scanned) mode.


3D glasses adaptor with line blanker prototype
An alternative is to build an external bit of hardware that simulates an interlaced video mode from a progressive one. The easiest way of doing that is to switch off the RGB signals on alternate scanlines, blanking odd scanlines in one frame and even scanlines in the next. This type of circuit is appropriately named a line blanker, and my current implementation is shown above. It sits between the PC and the monitor, and uses a pair of flip-flops which toggle state on vsync or hsync signals from the PC. The output from the vsync flip-flop is used to control which eye is open and which is shut on the LCD glasses, and is also combined with the hsync flip-flop to switch the RGB signal lines on or off on alternate lines using a THS7375 video amplifier. Unfortunately, this amplifier is only available as TSSOP, which isn't much fun to solder if you don't have the proper equipment; I made a stab at it with a regular iron, the smallest tip I could find, lots of no-clean flux and some solder braid. I have been informed that solder paste makes things considerably easier, so will have to try that next time.

My cheap LCD glasses lack any form of internal circuitry, merely offering two LCD panels wired directly to a 3.5mm stereo jack, and so I'm using the 4030 exclusive-OR gate oscillator circuit to drive them.

The adaptor provides one switch to swap the left and right eyes in case they are reversed, and another is provided to disable the line blanking circuit (useful for genuine interlaced video modes or alternate frame 3D). You can download a schematic of the circuit here as a PDF.

I've been using these glasses to play Quake in 3D, which is good fun but an experience that was sadly marred by a number of bugs and quirks in Quake's 3D mode.


WinQuake, demonstrating the crosshair bug and excessive stereo separation of the weapon
The most obvious problems in the above screenshot are the migratory crosshair (appearing 25% of the way down the screen instead of vertically centred) and the excessive stereo separation of the player's weapon.

If the console variable LCD_X is non-zero, Quake halves the viewport height then doubles what it thinks is the stride of the graphics buffer. This causes it to skip every other scanline when rendering. Instead of rendering once, as normal, it translates the camera in one direction, renders, then offsets the start of the graphics buffer by one scanline, translates the camera in the other direction then renders again. This results in the two views (one for each eye) being interleaved into a single image.

The crosshair is added after the 3D view is rendered (in fact, Quake just prints a '+' sign in the middle of the screen using its text routines), which explains its incorrect position - Quake doesn't take the previously halved height of the display into consideration, causing the crosshair to be drawn with a vertical position of half of half the height of the screen. That's pretty easy to fix - if LCD_X is non-zero, multiply all previously halved heights and Y offsets by two before rendering the crosshair to compensate.


WinQuake, demonstrating the DirectDraw corruption bug
A slightly more serious bug is illustrated above. When using the DirectDraw renderer (the default in full-screen mode), the display is corrupted. This can be fixed by passing -dibonly to the engine, but it would be nice to fix it.

After a bit of digging, it appeared that the vid structure, which stores fields such as the address of the graphics buffer and its stride, was being modified between calls to the renderer. It seemed to be reverting to the actual properties of the graphics buffer (i.e. it pointed to the top of the buffer and stored the correct stride of the image, not the doubled one). Further digging identified VID_LockBuffer() as the culprit; this does nothing if you're using the dib rendering mode, but locks the buffer and updates the vid structure in other access modes. Fortunately, you can call this function as many times as you like (as long as you call VID_UnlockBuffer() a corresponding number of times) - it only locks the surface and updates vid the first time you call it. By surrounding the entire 3D rendering routine in a VID_LockBuffer()...VID_UnlockBuffer() pair, vid is left well alone, and Quake renders correctly in full-screen once again.

The final issue was the extreme stereo separation of the player weapon, caused by its proximity to the camera - it does make the game quite uncomfortable to play. The game moves the camera and weapon to the player's position, then applies some simple transformations to implement view/weapon bobbing, before rendering anything. Applying the same camera offset and rotation to the player weapon as the camera when generating the two 3D views put the weapon slap bang in the middle of the screen, as it would appear in regular "2D" Quake. This gives it the impression of a carboard cutout, and can put it behind/"inside" walls and floors when you walk up to them; I've added a console variable, LCD_VIEWMODEL_SCALE, that can be used to interpolate between the default 3D WinQuake view (value: 1) and the cardboard cutout view (value: 0).


WinQuake with the 3D fixes applied
You can download the replacement WinQuake from here - you can just overwrite any existing executable. (You will also need the VC++ 2008 SP1 runtimes, if you do not already have them). Source code is included, and should build in VC++ 2008 SP1 (MASM only appears to be included in SP1, which is required to compile Quake's extensive collection of assembly source files).

If you don't have a copy of Quake, I recorded its looping demos in 3D and uploaded them to YouTube. This was before I made the above fixes, so there's no crosshair or player weapon model in the videos - if you have access to YouTube-compatible 3D glasses or crossable eyes, click here. [smile]

benryves

benryves

 

IM-me wireless terminal

A recent post on Hack a Day alerted me the to the IM-me, a device designed to be used with a web-based IM service that communicated with the PC via a USB wireless adaptor.


According to Hunter Davis, the body of the messages were sent between the PC and the IM-me are in plain text. This sounded like a good start to me, so I picked one up from Amazon UK for GBP7.49 (they're now available for even less than that). You get a lot of electronics for that price; there's a CC1110F32 microcontroller inside (the chips inside the device and its wireless adaptor are clearly marked - no nameless blob of epoxy that you might have expected from the price) and Dave has poked around the insides of his and has mapped the contact pads exposed via the battery compartment to the debug port on the microcontroller. You could use this debug port to overwrite the stock firmware with your own if the fancy took you. However, I'm more interested in seeing what I can do with the device without writing my own firmware for it.

The wireless adaptor shows up in Windows a simple USB HID, so I installed SnoopyPro and logged a chat session with myself. Fortunately, there is indeed no obfuscation or encryption to the structure of messages. I have worked on a C# library that handles most of the different message types (no group chat yet, only direct contact-to-contact) and written up what I've found here. The C# code can be found here, though it is not especially robust yet.


I think that my main problem is a poor grasp of asynchronous I/O. I read data asynchronously, but write synchronously, and don't currently do anything to protect against my code "speaking over" the incoming data. If you output data when the device is half way through sending a packet, it seems to ignore the data you're sending it. In the case of long messages, which are made up of multiple packets sent in rapid succession, they don't appear to ever reach the device. The USB device responds with a single 0 byte after a packet is written to it, which I don't currently wait for. I'm not sure how you can, when mixing asynchronous reading and synchronous writing, so if anyone has any suggestions or links to reading material I'd greatly appreciate it!

I have no intention of going near the existing IM-me web service - being able to use the IM-me as a general-purpose wireless terminal to talk to your own software opens up a wealth of possibilities. You could set it up to notify you of new emails, read RSS feeds, post updates to social networking sites, use it as a home automation console, remote control a media PC... You may wish to paint it black first, though!

Addendum: Whoops, after refactoring some code I broke the checksum generation. It appears that the IM-me ignores the checksum when receiving messages. I have stuck a brief pause between each byte written to the device and a slightly longer one between each packet sent to the device, and I can now send long messages to it.

benryves

benryves

 

Ejecting discs from a damaged camcorder with a remote control

I hope that those of you who celebrate it had a good Christmas break and will have an excellent new year!

I recently attempted to repair a DVD camcorder that had been dropped; the eject button no longer worked, though the disc could be ejected by connecting to camera to a PC, right-clicking the DVD drive that subsequently appears in Explorer, then selecting Eject.

I started by removing all of the screws around the affected area, but the plastic casing remained strongly held together by some mysterious internal force. I removed more and more screws, but it soon became apparent that the only way to get into the camera would be to force it open - not being my camera, I didn't feel comfortable doing so, as the rest of the camera worked well and I didn't want to damage any fragile internal mechanisms. I couldn't find any dismantling guides online, so gave up on the idea of fixing the button.

Fortunately, I own the same model of camcorder - a Panasonic VDR-D250 - myself. With my interest in infrared remote controls I had previously found information about the Panasonic protocol it uses. The supplied remote control only has a few simple buttons on it (no eject button, sadly), but I reckoned that the camcorder may accept a number of other commands that the stock remote didn't include.


I started by modifying a universal remote control program for the TI-83+ that I had previous written to allow me to send specific commands to the camcorder, then ran through all of the possible command IDs, noting down those that appeared to have some effect. Eventually I had a pretty decent list, albeit one with quite a few gaps in it. Fortunately, I had found the Eject button code, along with codes to switch mode (which is done on the camera by rotating a mode dial), one that powers the camcorder off, another that appears to restart the camera and another one that resets all settings (not so useful, that one).

Having found the eject code, I set about building a dedicated remote control. I picked the ATtiny13 microcontroller as a base, as that's a more than capable microcontroller with its 9.6MHz internal clock, 1KB program memory, 64 bytes SRAM and 3V operation.


I was a bit surprised to see that AVR-GCC supports the ATtiny13, and whilst C may seem overkill for such a project I'll gladly take advantage of anything that makes my life easier. [smile]

// Requisite header files.
#include
#include

// Frequency of the IR carrier signal (Hertz).
#define F_IR_CARRIER (37000)

// Timing of the data bits (microseconds).
#define T_DX_MARK (440)
#define T_D0_SPACE (440)
#define T_D1_SPACE (1310)

// Timing of the lead-in and lead-out bits (microseconds).
#define T_LEAD_IN_MARK (3500)
#define T_LEAD_IN_SPACE (1750)
#define T_LEAD_OUT_MARK (440)
#define T_LEAD_OUT_SPACE (74000)

// Commands definitions.
#define OEM_DEVICE_1_CODE (2)
#define OEM_DEVICE_2_CODE (32)
#define CAMCORDER_DEVICE_ID (112)
#define CAMCORDER_SUB_DEVICE_ID (40)
#define CAMCORDER_COMMAND_EJECT (1)

// Transmits a single unformatted byte.
void panasonic_send_byte(uint8_t value) {
// Send eight data bits.
for (uint8_t bit = 0; bit 8; ++bit, value >>= 1) {
// Send the mark/burst.
DDRB |= _BV(1);
_delay_us(T_DX_MARK);
// Send the space.
DDRB &= (uint8_t)~_BV(1);
_delay_us(T_D0_SPACE);
// Extend the space if it's a "1" data bit.
if (value & (uint8_t)1) {
_delay_us(T_D1_SPACE - T_D0_SPACE);
}
}
}

// Transmits a formatted command packet to the IR device.
void panasonic_send_command(uint8_t oem_device_code_1, uint8_t oem_device_code_2, uint8_t device_code, uint8_t sub_device_code, uint8_t command) {
// Send the lead in.
DDRB |= _BV(1);
_delay_us(T_LEAD_IN_MARK);
DDRB &= (uint8_t)~_BV(1);
_delay_us(T_LEAD_IN_SPACE);

// Send the five command bytes.
panasonic_send_byte(oem_device_code_1);
panasonic_send_byte(oem_device_code_2);
panasonic_send_byte(device_code);
panasonic_send_byte(sub_device_code);
panasonic_send_byte(command);

// Send the checksum.
panasonic_send_byte(device_code ^ sub_device_code ^ command);

// Send the lead out.
DDRB |= _BV(1);
_delay_us(T_LEAD_OUT_MARK);
DDRB &= (uint8_t)~_BV(1);
_delay_us(T_LEAD_OUT_SPACE);
}

// Main program entry point.
int main(void) {

TCCR0A |= _BV(COM0B0) | _BV(WGM01); // Toggle OC0B when on CTC reload. Use CTC mode.
TCCR0B |= _BV(CS00); // Set clock source to CPU clock/1.
OCR0A = (F_CPU / F_IR_CARRIER / 2) - 1; // Set the CTC reload value to generate an IR signal at the correct carrier frequency.

// Send the "eject" command ad infinitum.
for(;;) {
panasonic_send_command(OEM_DEVICE_1_CODE, OEM_DEVICE_2_CODE, CAMCORDER_DEVICE_ID, CAMCORDER_SUB_DEVICE_ID, CAMCORDER_COMMAND_EJECT);
}
}

The code is about as simple as the circuit. IR signals are transmitted as carefully timed bursts of a particular carrier frequency (37kHz in this case). For example, to send a "0" bit 440uS of this 37kHz signal are sent followed by 440uS of silence. To send a "1" bit, 440uS of carrier signal are sent as before, but a 1310uS period of silence follows it.

The AVR's timer is used to generate a ~37kHz carrier signal. The timer is an eight-bit counter that counts up at a user-defined rate (in my case I've chosen to increment the counter by one every CPU clock cycle). I've configured it to invert the output level of pin OC0B and reset every time it hits a particular value. By setting whether this pin is an output or an input the output of a burst of 37kHz IR signal or silence can be selected. Simple delay loops, generated with the helper function _delay_us, are used to time the transmission of data bits.


The final step was to assemble the circuit on stripboard and install it in a smallish project box. I've put the switch adjacent to the LED for two reasons; to conserve space and to protect it a little from accidentally being pressed by the protruding LED bezel.

Building a single-button remote control is a relatively straightforward affair, so whilst the above code has a very specific purpose it should be easy enough to modify it to control other devices.

benryves

benryves

 

Playing VGMs on an STM8S

Following the STM8S tutorial in my previous post, I've tried to put the chip to some practical use. My initial experiments into producing a video signal proved unsuccessful; I managed a static image using hard-coded delay loops, but when trying to use interrupts to trigger the generation of scanlines the timing was all wrong and without an oscilloscope or a working simulator I couldn't find out what was wrong. I decided to turn my attention from picture to sound.


VGM files store game music by logging the data written to the sound chips inside the console or computer directly along with the delay between writes. This results in reasonably small files that are capable of producing excellent sound quality, depending on the way the sound chips are emulated (or, in some cases, ">not emulated).

I've chosen to focus on the SN76489, a simple sound chip found in a variety of machines including the Sega Master System and BBC Micro. Three of its four sound channels are simple square wave tones, implemented as a 12-bit decrementing counter that flips the state of its output every time it underflows and is reset. Changing the value that is preloaded into the counter when it is reset changes the period of the output square wave, resulting in a change of pitch.


The fourth channel proves rather more of a challenge. It uses a shift register (15- or 16-bit depending on the particular version of the chip) instead of a simple tone counter, and has two modes. When generating periodic noise a single bit shuttles around the shift register, generating a 1/15th or 1/16th duty cycle square wave. This has effect of producing a lower pitch with a distinctive "buzzy" timbre. The other mode is white noise, which uses a feedback system to generate pseudo-random noise.

The emulated SN76489, or PSG, has been implemented in two parts. The first is an interrupt handler written in STM8S assembly for speed. This is executed approximately 44,100 times a second (44.1kHz is the internal time step used in VGM files) and is used to update the internal PSG counters and shift register and generate the output level for that particular sample. Two output levels are generated as I've implemented the Game Gear's stereo extension to the PSG (this simply lets you switch individual channels on or off for each ear). These levels are loaded into capture compare registers for TIM2, which is used in PWM mode to generate the analogue output signals.

The rest of the code is written in C. This includes the second part of the emulated PSG, which handles bytes written to the PSG and updates its internal registers as appropriate.


Due to a 16KB limitation with the free version of the Cosmic compiler (and the 32K physical limitation of the microcontroller itself) the VGM file is stored on external EEPROMs which are accessed over the I2C bus via the microcontroller's I2C peripheral. As I don't have any large single EEPROMs, I've used two 32KB EEPROMs, one at address 0xA0 and the other at 0xA2. When the read pointer overflows one EEPROM it automatically steps to the next EEPROM. In theory any size could be supported using this code, but I've used 16-bit variables for all of the file pointers introducing a 64KB limit - this should be easily fixable, but I don't own enough memory to test the code myself, so I've left it as it is for the moment.

// The program I use to split VGM files into 32KB chunks.
// Bear in mind that most VGMs are compressed (VGZ): you'll need to decompress them first.
// You can use 7-zip to do so.
using System.IO;
class Program {
static void Main(string[] args) {
var SourceFile = @"D:\Documents\Documents\VGM\StrykersRun-title";
using (var r = new BinaryReader(File.OpenRead(SourceFile))) {
for (var i = 0; i int.MaxValue; ++i) {
var data = r.ReadBytes(32 * 1024);
if (data.Length == 0) break;
File.WriteAllBytes(string.Format("{0} [{1}].bin", Path.GetFileNameWithoutExtension(SourceFile), i), data);
}
}
}
}

To take advantage of the delay between PSG accesses I've implemented a very simple buffering system that queues up a few bytes in advance from the EEPROM. This works well for music, but sampled audio (which involves updating the PSG very rapidly) doesn't work as the code spends too much time waiting for data to be transferred from the EEPROMs.

I've included some recordings of the output below.
Firetrack (Nick Pelling, BBC Micro) Stryker's Run (Martin Galway, BBC Micro) Alien 3 - Episode 2 (Matt Furniss, Sega Master System) Sega Chess - White Wins (Matt Furniss, Sega Master System) Gunstar Heroes - Military on the Max-Power (Kazuo Hanzawa, Hitoshi Sakimoto, M.Yuzuno, M.Yoshida, Y.Mizusawa; Sega Game Gear) [Stereo] That Other Final Conflict (RushJet1) Alex Kidd: The Lost Stars - I'm the Miracle Ball (Sega Master System) [Sampled speech, doesn't work] The source code can be downloaded from here. If you do try to run it you'll find that it tends to hang when trying to initialise the EEPROM; this is due to the I2C bus being left in an active state by forcefully terminating the program before debugging. I find it helps to program the board, disconnect then reconnect the power supply to the EEPROMs to reset them, then hitting continue in the debugger.

benryves

benryves

 

STM8S-Discovery review and tutorial

STMicroelectronics recently released the STM8S-Discovery, an exceedingly cheap (RRP $7) evaluation kit for one of their 8-bit microcontrollers. It features the microcontroller itself (an STM8S105C6), running at up to 16MHz and offering 32KB of program memory, 2KB of RAM and 1KB of data EEPROM. This microcontroller has a solid set of on-board peripherals, including four timers (one advanced, one general-purpose, and one basic), SPI, I2C, UART, and ADC - so there are plenty of connectivity options. The device offers up to 38 general-purpose I/O pins.


The evaluation board is pretty feature-packed, and includes an integrated ST-LINK for programming and debugging over USB. The circuit board has been designed so that you can simply snap off the ST-LINK part if you'd like to use the microcontroller on its own. Standard 0.1" pin headers are provided to permit you to connect the board to other components, and a small wrapping area is also present. A jumper can be used to select 5V or 3.3V operation.

A green LED and a touch sensitive key are built into the evaluation board; the device comes pre-programmed with a demo program that flashes this LED at different rates when you touch the key.

All in all, a decent piece of kit - but what really sets it apart is its price. ST's recommended price is $7; I bought mine for GBP4.25 from Farnell. All you need to do is provide a USB A to USB B cable and download the various development tools (Windows only at the time of writing), datasheets and libraries.

First impressions

I really wasn't expecting much for GBP4.25, but am very impressed with the hardware. It's solidly built and comes attractively packaged, with clear instructions on the back of the box on how to run the demo program (just plug it in to a USB port!) and where to go next for development tools.

Unfortunately, what appears to be lacking at the moment - not surprisingly for a new product - is guidance for absolute beginners with ST's microcontrollers. Hopefully that will improve as more people get hold of these new kits, as their incredibly low price and self-contained nature makes them ideal for beginners (no need to build up a collection of parts or buy a programmer to get started, just plug and play). I personally found the documentation quite baffling, and judging by a thread on Hack a Day I wasn't the only one.

Disclaimer

I'm not an expert with ST's microcontrollers, but I have at least managed to get something running on the microcontroller after a few frustrating hours spent with the current documentation. The following code may not be the best, but it is simple and it should work (if I've missed anything out or not been clear enough anywhere, please let me know so I can fix it). It doesn't go into any great detail; my assumption is that if you can get some code you've written yourself running on your evaluation board and understand how to use the basic peripherals offered by the microcontroller to work with the board's LED and touch key you should be ready enough to dive into the official documentation and sample code yourself!

Getting started

If you visit the STM8S-Discovery page, you will see options to download various pieces of software and development tools. You'll need to download the following:
A C compiler (I'm currently using Cosmic's, but it's worth installing the Raisonance compiler too). The ST MCU Toolset, which includes the ST Visual Develop IDE. The application development package (this contains examples and relevant libraries to access the various peripherals of the chip). The STM8S105C6 datasheet (invaluable device specifications/information). The STM8S microcontroller family reference manual (a general overview of the STMS8 family, including documentation on the various on-board peripherals). The STM8S-Discovery evaluation board user manual (information specific to the evaluation board, including circuit diagrams and other specifications). There is a document that accompanies the development package, Developing and debugging your STM8S-DISCOVERY application code, which contains instructions on setting up a project - confusingly, these directions don't appear to apply to Raisonance's tool chain, and rely on copying and pasting files from the demo programs. You will need to register the compilers to be able to use them, and Cosmic's registration process is handled by a human so may take some time for you to receive your licence key.

Debugging one of the sample programs

One way to verify that everything is set up correctly and to try out the development tools is to build one of the sample programs included as part of the application development package. In this zip file you will find a directory named STM8S-Discovery_dev; extract this somewhere sensible. Run ST Visual Develop, and select File->Open Workspace. Open the file STM8S-Discovery_dev\Project\Discover\STVD\Cosmic\Discover.stw - this is the project that came pre-loaded onto the evaluation board. If you click Build->Build it should crunch away and after a few seconds should report that there were no errors.

We'll now need to set up the IDE to use your evaluation board's ST-LINK as its debugging instrument. Click Debug instrument->Target Settings and select Swim ST-Link in the dropdown. You can leave the other settings as they are; click OK to close the dialog.


If you now click Debug->Start Debugging (or the blue "D" in the toolbar) the IDE should program the evaluation board and then enter the debugger in "Break" mode. Press Continue (F5) to start running the program; at this point you should be able to use the touch sensitive key on the board to change the rate at which the green LED flashes. When you're done, click Debug->Stop Debugging (or the red cross button in the toolbar) to stop debugging.


The debugger shouldn't be especially surprising to anyone who has used a graphical debugger (e.g. Visual Studio) before. Try going to line 148 in main.c - BlinkSpeed++; - and selecting Edit->Insert/Remove Breakpoint. Start debugging as before, and you'll notice that when you touch the key this time the debugger breaks on that line. Click Debug->Continue and the program will continue. Marvellous - all pretty intuitive thus far.

Creating your own project

Creating your own project is a rather more involved process, as there's quite a lot you need to set up first. Hopefully this step-by-step guide should help!

Firstly, click File->New Workspace, and select Create Workspace and Project from the dialog that appears. Now, we need to store our workspace (analagous to a solution in Visual Studio) somewhere; create a new directory for the workspace that is preferably not inside Program Files and give the workspace a name. I'm going to go for "Blinkenlight" as my workspace name. Create a new project with the same name as the solution name. It should default to the same directory; keep this as it is. Select STM8 Cosmic as the toolchain. Select STM8S105C6 as the microcontroller. With that done, you should have a shiny new workspace and project containing two files - main.c, containing the entry point for your application, and stm8_interrupt_vector.c, used to associate interrupt requests with interrupt service routines - more on those later. If you build the project and try to debug it you'll note that it does precisely nothing of use. Let's make it do something useful!

Adding the standard firmware library

To access the various peripherals of the microcontroller, ST have provided an extensive standard firmware library. You will need to download this from the STM8S documents and files page; it's the zip archive named STM8S firmware library. Open the zip archive, and copy the FWLib\library folder to your own project folder. You may wish to rename the library folder FWLib, so you should end up with the two folders Project\FWLib\inc and Project\FWLib\src.

There is a folder named project in the firmware library download - copy stm8s_conf.h from this folder into the root of your project folder.

Now, return to ST Visual Develop. Use the workspace panel to the left to create two new folders in your project - Source Files\FWLib and Include Files\FWLib. Add all of the files from FWLib\inc to Include Files\FWLib and stm8s_conf.h to the root of Include Files. You should now have something that looks like this:


You will also need to inform the firmware library that you are using an STM8S105 microcontroller as opposed to the default STM8S208. Here are two ways of doing this - pick whichever seems easiest to you.
Open stm8s.h in Project\FWLib\inc, comment out #define STM8S208 and uncomment #define STM8S105 near the top of the file. Pass the definition directly to the compiler by clicking Project->Settings, selecting the C Compiler tab and typing STM8S105 into the Preprocessor Definitions field. You will need to do this twice; once for the Debug configuration and once for Release (select the configuration with the drop-down box in the top left).
Illuminating the LED using GPIO

The cathode of the evaluation board's LED is connected to PD0 on the microcontroller - that is, pin 0 of port D. By driving this pin low we could therefore illuminate the LED. If you consult the documentation for the standard firmware library - it's the stm8s_fwlib_um.chm file in the zip archive - you can see a number of helper functions dedicated to GPIO, or "general-purpose I/O". To use the GPIO functions, we need to do two things:
Add FWLib\src\stm8s_gpio.c to the project under Source Files\FWLib. Enable inclusion of the relevant GPIO header files, achieved by uncommenting #define _GPIO (1) in stms8_conf.h Both steps will need to be carried out whenever you want to use a new peripheral (e.g. a timer or the UART). Once that's done, you can modify main.c to read as follows:

#include "stm8s.h"

int main(void) {

// Reset ("de-initialise") GPIO port D.
GPIO_DeInit(GPIOD);

// Initialise pin 0 of port D by setting it as:
// - an output pin,
// - using a push-pull driver,
// - at a low logic level (0V), and
// - 10MHz.
GPIO_Init(GPIOD, GPIO_PIN_0, GPIO_MODE_OUT_PP_LOW_FAST);

// Infinite loop.
for(;;);
}

There are more comments than code there - using the firmware library makes life rather easy, once it's set up! If you start debugging that, you'll note that the LED does indeed light up. It's not much, but it's a sign of life.

Flashing the LED using a delay loop

The GPIO library provides a handy GPIO_WriteReverse() function, which inverts the state of a pin. By toggling PD0, we can make the LED flash. To slow this flashing down at a rate we can see, a delay loop is introduced that delays for 50,000 nops, plus overhead of the for loop structure, between calls to GPIO_WriteReverse().

#include "stm8s.h"

int main(void) {

// Reset ("de-initialise") GPIO port D.
GPIO_DeInit(GPIOD);

// Initialise pin 0 of port D.
GPIO_Init(GPIOD, GPIO_PIN_0, GPIO_MODE_OUT_PP_LOW_FAST);

// Infinite loop.
for(;;) {

// Delay for a short while.
u16 d;
for (d = 0; d 50000; ++d) {
// Without a nop() in here, the entire loop would be optimised away!
nop();
}

// Invert the LED pin's state to flash it.
GPIO_WriteReverse(GPIOD, GPIO_PIN_0);
}
}

The reason for the nop() is that without it, the compiler optimises away the entire for loop as it does nothing useful.

Well, that's a bit more dynamic, but surely there's a better way to do this than a hard-coded delay loop?

Timers

Timers - of which the STM8S has four with varying capabilities - are an extremely versatile peripheral. They are typically based around a counter, which counts up or down, and various events can be triggered when this counter reaches particular values. We'll start here by using the TIM3 peripheral.

To use TIM3 we need to perform the following steps, as we did previously for GPIO:
Add FWLib\src\stm8s_tim3.c to the project under Source Files\FWLib. Uncomment #define _TIM3 (1) in stms8_conf.h
By default, the microcontroller uses its internal 16MHz RC oscillator ("HSI", or high-speed internal) divided by eight as a clock source. This results in a base timer frequency of 2MHz. When configuring a timer, you can specify a prescaler to further divide the clock frequency - if you were to divide the 2MHz clock frequency by 2000, the timer would count up once every millisecond.

TIM3 restricts the prescaler to powers of two between 1 and 32,768, so we'll use a prescaler of 2048 (that's close enough for jazz). You also need to specify a timer period; this is the value up to which the timer will count before resetting itself. With a period of 999, the timer will run from 0 to 999 before resetting - approximately one thousand milliseconds, or one second, from start to finish.

// Reset ("de-initialise") TIM3.
TIM3_DeInit();

// Set TIM3 to use a prescaler of 2048 and have a period of 999.
TIM3_TimeBaseInit(TIM3_PRESCALER_2048, 999);

// Enable TIM3.
TIM3_Cmd(ENABLE);

Using the standard firmware library allows for easy timer configuration. Now that we have a timer running, we can query its counter value to flash the LED - if it's in the range 0~499, switch the LED on; if it's in the range 500~999, switch the LED off. Building on the ealier source code, here's a program that does just that:

#include "stm8s.h"

int main(void) {

// Reset ("de-initialise") GPIO port D.
GPIO_DeInit(GPIOD);
// Initialise pin 0 of port D.
GPIO_Init(GPIOD, GPIO_PIN_0, GPIO_MODE_OUT_PP_LOW_FAST);

// Reset ("de-initialise") TIM3.
TIM3_DeInit();
// Set TIM3 to use a prescaler of 2048 and have a period of 999.
TIM3_TimeBaseInit(TIM3_PRESCALER_2048, 999);
// Enable TIM3.
TIM3_Cmd(ENABLE);

// Infinite loop.
for(;;) {
if (TIM3_GetCounter() 500) {
// Output a low on the LED pin to illuminate it.
GPIO_WriteLow(GPIOD, GPIO_PIN_0);
} else {
// Output a high on the LED pin to switch it off.
GPIO_WriteHigh(GPIOD, GPIO_PIN_0);
}
}
}


Pulse-width modulation for flashing

One of the many features of these timers is the ability to generate pulse-width modulation - PWM - output on dedicated pins. When this feature is enabled, the timer will set the output pin to one logic level when it starts or restarts and to another when it reaches a used-defined threshold. This is effectively what we're doing in our current program, just manually - far better if the timer could do it for us automatically!

The green LED is connected to PD0, which also acts as TIM3_CH2, or TIM3's channel 2. We can remove most of the code from our previous program, including the GPIO code, leaving us with the following:

#include "stm8s.h"

int main(void) {

// Reset ("de-initialise") TIM3.
TIM3_DeInit();

// Set TIM3 to use a prescaler of 2048 and have a period of 999.
TIM3_TimeBaseInit(TIM3_PRESCALER_2048, 999);

// Initialise output channel 2 of TIM3, by setting:
// - PWM1 mode (starts activated, deactivates when capture compare value is hit),
// - output is enabled,
// - capture compare value of 500, and
// - an active signal is low (0V).
TIM3_OC2Init(TIM3_OCMODE_PWM1, TIM3_OUTPUTSTATE_ENABLE, 500, TIM3_OCPOLARITY_LOW);

// Enable TIM3.
TIM3_Cmd(ENABLE);

// Infinite loop.
for(;;);
}

In PWM1 mode, the output starts in the activated state. As we have specified that TIM3_OCPOLARITY_LOW is an activated state, this means that the output will start at a logic low (the LED will be illuminated). When the capture compare value (500) is reached, the output will switch to the deactivated state (logic high) and the LED will switch off. If you run this program as before you will see that the LED does indeed flash on and off automatically.

Pulse-width modulation to change brightness

As the LED is on for approximately 500ms and off for approximately 500ms it is on half of the time. On average, therefore, it is at half its possible brightness. If you modify the 500 in the TIM3_OC2Init function call to 250 and run the program again you will see that on for a quarter of the possible time, and at 750 it is on for three quarters of the possible time. By increasing the rate at which the LED flashes so that it appears to be continuously lit we can control its apparent brightness by adjusting the relative amount of time it is switched on in comparison to the time it is switched off.

We can increase the rate at which the LED flashes by reducing the prescaler of TIM3. Try changing the TIM3_TimeBaseInit call to use TIM3_PRESCALER_16 instead of TIM3_PRESCALER_2048, and change the capture compare value in the TIM3_OC2Init call to 100. When you run your program, the LED on the board will appear to be dimly lit. If you pick up the board and very carefully shake it from side to side you should be able to see that the LED is flashing from the dotted trace it leaves in the air. If you drop the prescaler all the way down to TIM3_PRESCALER_1 you will find that you have to shake the board much faster, but take care not to damage anything!

The brightness of the LED can be modified at runtime by changing the value of the capture compare register with the TIM3_SetCompare2 function.

#include "stm8s.h"

// Short delay loop.
void delay(void) {
u16 d;
for (d = 0; d 150; ++d) {
nop();
}
}

int main(void) {

// Reset ("de-initialise") TIM3.
TIM3_DeInit();

// Set TIM3 to use a prescaler of 1 and have a period of 999.
TIM3_TimeBaseInit(TIM3_PRESCALER_1, 999);

// Initialise output channel 2 of TIM3.
TIM3_OC2Init(TIM3_OCMODE_PWM1, TIM3_OUTPUTSTATE_ENABLE, 0, TIM3_OCPOLARITY_LOW);

// Enable TIM3.
TIM3_Cmd(ENABLE);

// Infinite loop.
for(;;) {

u16 brightness;

// Set the brightness from 0 to 999 in a loop (fade up).
for (brightness = 0; brightness 1000; ++brightness) {

// Set the brightness of the LED by modifying the capture compare register
// for TIM3's channel 2.
TIM3_SetCompare2(brightness);

// Delay a short while.
delay();
}

// Set the brightness from 1000 to 1 in a loop (fade down).
for (brightness = 1000; brightness > 0; --brightness) {

// Set the brightness of the LED.
TIM3_SetCompare2(brightness);

// Delay a short while.
delay();
}
}
}

The above program fades the LED up from the minimum brightness to the maximum brightness then back down again in an infinite loop. It also reintroduces our old enemy, the delay loop, which leads us neatly on to the next subject - interrupts.

Interrupts

Interrupts provide a way to respond to events in a way that doesn't require that we constantly check (poll) the event source. One such event is a timer overflowing - we can use this event to update the brightness of the LED every millisecond without having to poll a timer's counter manually or use delay loops.

We'll use TIM1 to generate the interrupt; it provides a few additional features that are not present on the other timers, but we'll need to keep TIM2 and TIM4 free for later. As before, you'll need to add stm8s_tim1.c to your project and uncomment #define _TIM1 (1) in stm8s_conf.h.

We'll start by adding some skeleton interrupt handler code and reference it in the interrupt vector table to ensure that it is called when the timer updates itself. Firstly, add the two following files to your project:

stm8s_it.c
#include "stm8s.h"
#include "stm8s_it.h"

void TIM1_UPD_OVF_TRG_BRK_IRQHandler(void) {
// TODO: Implement TIM1 update interrupt handler.
}

stm8s_it.h
#ifndef __STM8S_IT_H
#define __STM8S_IT_H

@far @interrupt void TIM1_UPD_OVF_TRG_BRK_IRQHandler(void);

#endif

These stm8s_it files contain the interrupt request handlers. Interrupt handler functions are called via the interrupt vector table, which is defined in the stm8_interrupt_vector.c file that was automatically generated when you created the project. Open this file and add #include "stm8s_it.h" to the top of it so that it can see your interrupt handler functions. According to the STM8S105xx datasheet the TIM1 update/overflow interrupt is mapped to IRQ 11, so scroll down the table of interrupt vectors and change NonHandledInterrupt on the line marked irq11 (some lines omitted for clarity):

struct interrupt_vector const _vectab[] = {
{0x82, (interrupt_handler_t)_stext}, /* reset */
{0x82, NonHandledInterrupt}, /* trap */
{0x82, NonHandledInterrupt}, /* irq0 */
/* [...] */
{0x82, NonHandledInterrupt}, /* irq10 */
{0x82, (interrupt_handler_t)TIM1_UPD_OVF_TRG_BRK_IRQHandler}, /* irq11 */
{0x82, NonHandledInterrupt}, /* irq12 */
/* [...] */
{0x82, NonHandledInterrupt}, /* irq29 */
};

Now we have that in place we can start writing the interrupt handler code. Internally, interrupts are signalled by setting a flag in a control register, which the microcontroller periodically checks. If you do not clear this flag the microcontroller will call your interrupt handler again as soon as you return from the function, so you must remember to do so - this is done with the TIM1_ClearITPendingBit(TIM1_IT_UPDATE) function. Using a variable to store the current brightness "direction" (positive to get brighter; negative to get dimmer) the LED brightness could be adjusted every time the timer overflowed using the following code:

stm8s_it.c
#include "stm8s.h"
#include "stm8s_it.h"

s16 brightness_direction = +1; // Start by getting brighter.

void TIM1_UPD_OVF_TRG_BRK_IRQHandler(void) {

// Get the current brightness.
u16 current_brightness = TIM3_GetCapture2();

// Check whether we've hit the maximum/minimum brightness yet.
if (brightness_direction > 0) {
// We're currently getting brighter.
if (current_brightness == 1000) {
// We're already at the maximum brightness; start getting darker.
brightness_direction = -1;
}
} else {
// We're currently getting dimmer.
if (current_brightness == 0) {
// We're already at the minimum brightness; start getting brighter.
brightness_direction = +1;
}
}

// Update the brightness of the LED according to the brightness "direction".
TIM3_SetCompare2(current_brightness + brightness_direction);

// Clear the interrupt pending bit for TIM1.
TIM1_ClearITPendingBit(TIM1_IT_UPDATE);
}

We also need to configure TIM1 to generate interrupts. This can be done with the TIM1_ITConfig function, in addition to the existing code used to configure TIM3:

main.c
#include "stm8s.h"

int main(void) {

// Reset ("de-initialise") TIM3.
TIM3_DeInit();
// Set TIM3 to use a prescaler of 1 and have a period of 999.
TIM3_TimeBaseInit(TIM3_PRESCALER_1, 999);
// Initialise output channel 2 of TIM3.
TIM3_OC2Init(TIM3_OCMODE_PWM1, TIM3_OUTPUTSTATE_ENABLE, 0, TIM3_OCPOLARITY_LOW);
// Enable TIM3.
TIM3_Cmd(ENABLE);

// Reset ("de-initialise") TIM1.
TIM1_DeInit();
// Set TIM1 to:
// - use an exact prescaler of 1000,
// - to count up,
// - to have a period of 1, and
// - to have a repetition counter of 0.
TIM1_TimeBaseInit(1000, TIM1_COUNTERMODE_UP, 1, 0);
// Set TIM1 to generate interrupts every time the counter overflows (every ms).
TIM1_ITConfig(TIM1_IT_UPDATE, ENABLE);
// Enable TIM1.
TIM1_Cmd(ENABLE);

// Enable interrupts (no, really).
enableInterrupts();

// Infinite loop.
for(;;);
}


Interrupts are globally disabled by default, hence the need to call enableInterrupts(). If you run this program, you should find that the LED fades in and out as before, but without the need for hacky delay loops. As you can see, TIM1 takes a few additional parameters to its TIM1_TimeBaseInit function; you aren't limited to powers of two for its prescaler, it can count in a number of different ways and you can specify a "repetition count" that will only update the timer registers after a given number of cycles of the counter (in this case, we've disabled that feature).

Touch key input

As well as an LED for output, the evaluation board provides a touch key for input. This requires considerably more computing power to handle than a conventional push switch, but is considerably more interesting! Thankfully, ST have provided a royalty-free library to handle touch sensing keys, sliders and wheels with their microcontrollers which we can use:
Visit the documents and files page again to download the STM8S Touch Sensing Library; it is packaged as an installer, which should be run. Go to the installation directory and copy Libraries\STM8_TouchSensing_Driver to your own project folder as you did for FWLib previously. Move Inc\STM8_TSL_RC_Configuration_TOADAPT.h to the root of your project folder and rename it STM8_TSL_RC_Configuration_TOADAPT.h (remove "_TOADAPT"). When you have copied the files, switch back to your project and follow these steps:
Create a folder Touch Sensing Library under Source Files and add all of the files in STM8_TouchSensing_Driver\Src apart from STM8_TSL_RC_MultiChannelKey.c to it. Create a folder Touch Sensing Library under Include Files and add all of the files in STM8_TouchSensing_Driver\Inc to it. Add STM8_TSL_RC_Configuration.h to the root of Include Files. Due to some functions needing to be aligned to even memory addresses, you will need to modify your linker settings. In the IDE, click Project->Settings and switch to the Linker tab. Set the Category dropdown to Input, expand the Code, Constants section and add a section named .TSL_IO_ALCODE with its options set to -r2. You will need to do this to both Debug and Release configurations.


Now we need to go and configure STM8_TSL_RC_Configuration.h for our particular hardware. Open this file, and make the following amendments:
TIMACQ will need to be changed to TIM2 as we're using TIM3 to drive our LED. TIMACQ_CNTR_ADD needs to be changed to 0x530A to match the change to TIM2. The touch key is attached to GPIO port C so LOADREF_PORT_ADDR needs to be changed to GPIOC_BaseAddress. The load reference is connected to pin PC2, so we need to change LOADREF_BIT to 0x04 (1 We only have one key, so change SCKEY_P1_KEY_COUNT to 1. The touch key input is connected to PC1, so leave SCKEY_P1_PORT_ADDR at GPIOC_BaseAddress and SCKEY_P1_A at 0x02 (1SCKEY_P1_B to SCKEY_P1_H) to 0. As we don't have any keys on a second port, set SCKEY_P2_KEY_COUNT, SCKEY_P2_PORT_ADDR and SCKEY_P2_A--SCKEY_P2_H to 0. As we don't have any multi-channel keys, set NUMBER_OF_MULTI_CHANNEL_KEYS to 0. The touch key electrodes are connected to PC1 and PC2, so set GPIOC_ELECTRODES_MASK to 0x0A (0b00001010). Set all of the other electrode masks to 0. Whew, quite a lot of work there! Now we've set that up, we can get programming. Try building your project; it should take a bit longer than before, but not emit any errors if you've set things up correctly!

The first thing we need to change in our program is to switch to running at 16MHz, a requirement of the touch sensing library. To do this, we need to use the CLK peripheral library; add stm8s_clk.c to Source Files\FWLib as before, and uncomment #define _CLK (1) in stm8s_conf.h. Now add CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1) to the start of your main() function to set the internal high-speed oscillator prescaler to 1 (it defaults to eight). If you now build and run your project you will notice that the LED fades up and down much faster - eight times faster, in fact. Change the TIM1 prescaler to 8000 to revert to the old speed:

#include "stm8s.h"

int main(void) {

// Set the internal high-speed oscillator to 1 to run at 16/1=16MHz.
CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1);

// Reset ("de-initialise") TIM3.
TIM3_DeInit();
// Set TIM3 to use a prescaler of 1 and have a period of 999.
TIM3_TimeBaseInit(TIM3_PRESCALER_1, 999);
// Initialise output channel 2 of TIM3.
TIM3_OC2Init(TIM3_OCMODE_PWM1, TIM3_OUTPUTSTATE_ENABLE, 0, TIM3_OCPOLARITY_LOW);
// Enable TIM3.
TIM3_Cmd(ENABLE);

// Reset ("de-initialise") TIM1.
TIM1_DeInit();
// Set TIM1 to use a prescaler of 8000 and to have a period of 1.
TIM1_TimeBaseInit(8000, TIM1_COUNTERMODE_UP, 1, 0);
// Set TIM1 to generate interrupts every time the counter overflows (every ms).
TIM1_ITConfig(TIM1_IT_UPDATE, ENABLE);
// Enable TIM1.
TIM1_Cmd(ENABLE);

// Enable interrupts.
enableInterrupts();

// Infinite loop.
for(;;);
}

Now we've got that organised, we can go ahead with using the touch sensing library. Start by adding #include "STM8_TSL_RC_API.h" to the top of main.c. We need to initialise the library and the touch key in our main function; add the following lines after the CLK_HSIPrescalerConfig call:

// Initialise the touch sensing library.
TSL_Init();
// Initialise the key (we only have one key).
sSCKeyInfo[0].Setting.b.IMPLEMENTED = 1; // It's implemented...
sSCKeyInfo[0].Setting.b.ENABLED = 1; // ...and enabled.

The touch sensing library makes use of a timer interrupt. We've set TIMTICK to TIM4, so need to attach the TIM4 update/overflow interrupt (IRQ 23) to TSL_Timer_ISR. Open stm8_interrupt_vector.c, add #include "STM8_TSL_RC_API.h" to the top of it, then modify the vector marked irq23:

#include "stm8s_it.h"
#include "STM8_TSL_RC_API.h"

/* [...] */

struct interrupt_vector const _vectab[] = {
{0x82, (interrupt_handler_t)_stext}, /* reset */
{0x82, NonHandledInterrupt}, /* trap */
{0x82, NonHandledInterrupt}, /* irq0 */
/* [...] */
{0x82, NonHandledInterrupt}, /* irq22 */
{0x82, (interrupt_handler_t)TSL_Timer_ISR}, /* irq23 */
{0x82, NonHandledInterrupt}, /* irq24 */
/* [...] */
{0x82, NonHandledInterrupt}, /* irq29 */
};

Some lines are, as before, omitted for clarity.

The infinite loop at the end of the program will need to be modified to call the TSL_Action function to update the touch sensing library's internal state machine, then check the state of the touch sensing library to see if there's any input to be processed:

// Infinite loop.
for(;;) {
// Update the touch sensing library's state machine.
TSL_Action();
// Check to see if something has happened, and that we're in the idle state before handling it.
if ((TSL_GlobalSetting.b.CHANGED) && (TSLState == TSL_IDLE_STATE)) {
// Clear the "something has changed" flag.
TSL_GlobalSetting.b.CHANGED = 0;
// Has our key been pressed/detected?
if (sSCKeyInfo[0].Setting.b.DETECTED) {
nop(); //
}
}
}

Set a breakpoint on the nop() line, then build and run the program. The LED will fade up and down as before, but if all has gone to plan touching the key should break execution on the nop() line. A slightly more useful program is shown below, modifying the current LED fading code to only fade out and using the touch key to set the LED to its maximum brightness when tapped.

main.c
#include "stm8s.h"
#include "STM8_TSL_RC_API.h"

int main(void) {

// Set the internal high-speed oscillator to 1 to run at 16/1=16MHz.
CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1);

// Initialise the touch sensing library.
TSL_Init();
// Initialise the key (we only have one key).
sSCKeyInfo[0].Setting.b.IMPLEMENTED = 1; // It's implemented...
sSCKeyInfo[0].Setting.b.ENABLED = 1; // ...and enabled.

// Reset ("de-initialise") TIM3.
TIM3_DeInit();
// Set TIM3 to use a prescaler of 1 and have a period of 999.
TIM3_TimeBaseInit(TIM3_PRESCALER_1, 999);
// Initialise output channel 2 of TIM3.
TIM3_OC2Init(TIM3_OCMODE_PWM1, TIM3_OUTPUTSTATE_ENABLE, 0, TIM3_OCPOLARITY_LOW);
// Enable TIM3.
TIM3_Cmd(ENABLE);

// Reset ("de-initialise") TIM1.
TIM1_DeInit();
// Set TIM1 to use a prescaler of 8000 and to have a period of 1.
TIM1_TimeBaseInit(8000, TIM1_COUNTERMODE_UP, 1, 0);
// Set TIM1 to generate interrupts every time the counter overflows (every ms).
TIM1_ITConfig(TIM1_IT_UPDATE, ENABLE);
// Enable TIM1.
TIM1_Cmd(ENABLE);

// Enable interrupts.
enableInterrupts();

// Infinite loop.
for(;;) {
// Update the touch sensing library's state machine.
TSL_Action();
// Check to see if something has happened, and that we're in the idle state before handling it.
if ((TSL_GlobalSetting.b.CHANGED) && (TSLState == TSL_IDLE_STATE)) {
// Clear the "something has changed" flag.
TSL_GlobalSetting.b.CHANGED = 0;
// Has our key been pressed/detected?
if (sSCKeyInfo[0].Setting.b.DETECTED) {
TIM3_SetCompare2(1000);
}
}
}
}

stm8s_it.c
#include "stm8s.h"
#include "stm8s_it.h"

void TIM1_UPD_OVF_TRG_BRK_IRQHandler(void) {

// Get the current brightness.
u16 current_brightness = TIM3_GetCapture2();

// If it's brighter than zero, dim it by one unit.
if (current_brightness > 0) {
TIM3_SetCompare2(current_brightness - 1);
}

// Clear the interrupt pending bit for TIM1.
TIM1_ClearITPendingBit(TIM1_IT_UPDATE);
}


Conclusion

Now that you've got this far, you should be able to delve into the documentation and samples provided by ST to find out more about this platform. If ST can keep the price as low as they currently recommend, then this is an extremely attractive platform for hobbyists, especially beginners as you don't need any additional tools barring a USB A to USB B cable - I have deliberately avoided interfacing with external components, for that reason. The hardware is extremely capable, low price or not, so I'm sure we'll see many interesting projects created with this board as a starting point!

Further Reading
Mr Foo has written a useful tutorial on mixing C and assembly. m_kisacanin has written a handy beginner's GPIO primer.

benryves

benryves

 

ATmega168 Snake

In addition to the Tetris game from the previous post, I've added an implementation of snake to the ATmega168 project.

">
Click to watch a gameplay video on YouTube.
Either game can be selected from a menu that appears when the circuit is powered on. To exit menus I've added a second fire button; this allows you to step back to the main menu to pick a different game if need be. The source code and binary can be downloaded as before.

I've written a number of different Snake implementations in the past. The early versions used a single array to represent every cell that the snake's body lay in (head as the first element, tail as the last element) that I would manually shift every frame and resize when the snake ate some food. This gets slower and slower as the snake gets longer, which isn't very good. When I wrote a version for the TI-83+ calculator in BBC BASIC, I switched to using a ring buffer with a pointer to the head element and another to the tail element that would be shunted along every frame, unless the snake ate some food in which case the tail pointer would stay where it was.

As I have even less memory on the ATmega168, I went for a different tactic again; by using "pretty" graphics for the various parts of the snake in the tilemap, I didn't need to store the snake's path anywhere other than this tilemap. That is, if I wanted to advance the tail one unit, I merely need to look at the current tile graphic being used to represent the tail (which will be pointing up, down, left or right) and follow it along to the tile in front of it. By inspecting this tile, I can see if the snake turned a corner at that point or went straight ahead and so adjust the tail position and graphic accordingly.

void advance_tail(void) {
// Find the current snake tail graphic.
char tail = tvtext_buffer[tail_y * TVTEXT_BUFFER_WIDTH + tail_x];
// Where is the body in relation to the tail?
int8_t body_x = tail_x, body_y = tail_y;
switch (tail) {
case FONT_SNAKE_TAIL_UP:
--body_y;
break;
case FONT_SNAKE_TAIL_DOWN:
++body_y;
break;
case FONT_SNAKE_TAIL_LEFT:
--body_x;
break;
case FONT_SNAKE_TAIL_RIGHT:
++body_x;
break;
}
// Ensure the body is on the buffer.
if (body_x if (body_x > WORLD_RIGHT) body_x = WORLD_LEFT;
if (body_y if (body_y > WORLD_BOTTOM) body_y = WORLD_TOP;
// Find the current body graphic.
char body = tvtext_buffer[body_y * TVTEXT_BUFFER_WIDTH + body_x];
// Is it a bend? If so, we'll need to rotate the tail graphic.
switch (body) {
case FONT_SNAKE_BODY_DOWN_RIGHT:
tail = (tail == FONT_SNAKE_TAIL_UP) ? FONT_SNAKE_TAIL_RIGHT : FONT_SNAKE_TAIL_DOWN;
break;
case FONT_SNAKE_BODY_DOWN_LEFT:
tail = (tail == FONT_SNAKE_TAIL_UP) ? FONT_SNAKE_TAIL_LEFT : FONT_SNAKE_TAIL_DOWN;
break;
case FONT_SNAKE_BODY_UP_RIGHT:
tail = (tail == FONT_SNAKE_TAIL_DOWN) ? FONT_SNAKE_TAIL_RIGHT : FONT_SNAKE_TAIL_UP;
break;
case FONT_SNAKE_BODY_UP_LEFT:
tail = (tail == FONT_SNAKE_TAIL_DOWN) ? FONT_SNAKE_TAIL_LEFT : FONT_SNAKE_TAIL_UP;
break;
}
// Erase the old tail.
tvtext_buffer[tail_y * TVTEXT_BUFFER_WIDTH + tail_x] = tvtext_cleared;
// Draw the new tail.
tail_x = body_x;
tail_y = body_y;
tvtext_buffer[tail_y * TVTEXT_BUFFER_WIDTH + tail_x] = tail;
}

Similar code is used to advance the head and draw the correct tile behind it.

On an unrelated note, I've released a version of BBC BASIC that should run on the Nspire. The Nspire has an emulator on it to run applications for other calculators, but this emulator doesn't implement undocumented instructions. The TI-83+/TI-84+ BBC BASIC host interface makes use of the sl1 instruction, which shifts a register left one bit and sets the least significant bit to 1. Unfortunately, when this code is run on an Nspire it triggers a crash. Apparently the quick fix I've implemented seems to have done the trick, so unless I hear any further bug reports I'll release the latest version formally soon!

benryves

benryves

 

ATmega168 Tetris

The tvText library I discussed last entry allows you to display text on a PAL TV in black and white using a 20MHz ATmega168 and a pair of resistors. If this doesn't sound terribly exciting, it's probably because it isn't. However, if you bear some limitations in mind and change the font, you can use this text output as a more general tile-mapping system and use it for games that employ simple graphics.


The new circuit, featuring five sloppily-wired input buttons.
I added five buttons to the test circuit -- up, down, left, right and fire -- to act as game input. This circuit is shown in the photograph above. I also added support for 8x8 characters alongside the existing 6x8 characters to the library, set as a compile-time option. This drops the number of characters per line from 32 to 24, but having square tiles makes producing graphics much easier. The reduction in size of the text buffer also frees up more of the precious 1KB of SRAM for the game!


Even though it was always recommended as an excellent game for beginners to write, I don't believe I've ever written a Tetris clone before. Its simple block graphics makes it an ideal candidate for this system, and it always helps to work on a game that's fun to play. Armed with a Game Boy and a stopwatch I attempted to recreate a moderately faithful version of what is probably the most popular rendition of the game.

">
Click to watch a gameplay video on YouTube.
I think the result plays pretty well, but don't take my word for it -- if you have an ATmega168 lying around, you can download the source and binaries here.

benryves

benryves

 

USB joypads and text on your TV courtesy of an ATmega168

Nearly a month since my last update - my, how time flies when you're having fun (or a heavy workload).

I ended up building myself a cheap and cheerful SI Prog programmer for AVR development. After installing the development tools, scanning through the documentation and writing the microcontroller equivalent of Hello, World (flashing an LED on and off) I needed to find a suitable project. The first one was getting to grips with V-USB, a software USB implementation for AVRs. All you need for this are a couple of I/O pins, a few configuration file changes to set your USB device's vendor ID, product ID and device class, and a few lines of C code to actually implement your device. I attached six tactile switches to an ATmega168 and made the most uncomfortable USB joypad I've ever used. I managed two levels of Sonic the Hedgehog before my thumbs admitted defeat, but it's nice to know that building USB devices is very easy with an AVR.

#include
#include
#include
#include
#include

#include "usbdrv.h"

/* Joystick port bits */
#define JOY_1 (10)
#define JOY_2 (11)
#define JOY_UP (12)
#define JOY_DOWN (13)
#define JOY_LEFT (14)
#define JOY_RIGHT (15)

/* USB HID report descriptor */
PROGMEM char usbHidReportDescriptor[USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH] = {
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x05, // USAGE (Game Pad)
0xa1, 0x01, // COLLECTION (Application)
0x09, 0x01, // USAGE (Pointer)
0xa1, 0x00, // COLLECTION (Physical)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xff, 0x00, // LOGICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x02, // REPORT_COUNT (2)
0x81, 0x02, // INPUT (Data,Var,Abs)
0xc0, // END_COLLECTION
0x05, 0x09, // USAGE_PAGE (Button)
0x19, 0x01, // USAGE_MINIMUM (Button 1)
0x29, 0x02, // USAGE_MAXIMUM (Button 2)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data,Var,Abs)
0xc0 // END_COLLECTION
};

static uchar reportBuffer[3]; /* Buffer for HID reports */
static uchar idleRate; /* 4 ms units */

uchar usbFunctionSetup(uchar data[8]) {
usbRequest_t *rq = (void*)data;
usbMsgPtr = reportBuffer;
if ((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS) {
switch (rq->bRequest) {
case USBRQ_HID_GET_REPORT:
return sizeof(reportBuffer);
case USBRQ_HID_GET_IDLE:
usbMsgPtr = &idleRate;
return 1;
case USBRQ_HID_SET_IDLE:
idleRate = rq->wValue.bytes[1];
break;
}
}
return 0;
}

ISR(TIMER0_OVF_vect) {

/* Fetch input */
uchar input = ~PINC;

/* X-axis */
switch (input & (JOY_LEFT | JOY_RIGHT)) {
case JOY_LEFT:
reportBuffer[0] = 0;
break;
case JOY_RIGHT:
reportBuffer[0] = 255;
break;
default:
reportBuffer[0] = 128;
break;
}

/* Y-axis */
switch (input & (JOY_UP | JOY_DOWN)) {
case JOY_UP:
reportBuffer[1] = 0;
break;
case JOY_DOWN:
reportBuffer[1] = 255;
break;
default:
reportBuffer[1] = 128;
break;
}

/* Buttons */
reportBuffer[2] = input & (JOY_1 | JOY_2);

usbPoll();
usbSetInterrupt(reportBuffer, sizeof(reportBuffer));
};

int main(void) {

usbInit(); /* Initialise USB. */

PORTC = 0b00111111; /* Pull high PORTC0..PORTC5 */

TCCR0B = 0b00000101; /* CS2..CS0 = 101: prescaler = /1024 */
TIMSK0 |= (1 /* Enable timer 0 overflow interrupt. */
sei(); /* Enable global interrupts. */

for (;;) {
/* Infinite loop */
}
}

I should only really call usbSetInterrupt when a button or axis has changed, rather than every loop, but the above code works as is.

One thing that always bothers me when it comes to electronic projects is the difficulty of providing text output. LCDs are generally quite expensive and low resolution, and typically require a great many pins to drive them. Video display processor chips are difficult to find, and appear to require quite complex external circuitry (the best thing I've found thus far are some TMS9918 chips being sold as spares for MSX computers). Having briefly experimented with generating PAL video signals in software before, I thought I'd try the two-resistor approach to getting PAL video output on an ATmega168.

I had a hunt around and found AVGA, which is close to what I wanted - video output from an AVR using cheap hardware. However, it outputs RGB directly, and I don't own a TV or RGB converter so couldn't use that - all I have is a VGA box (accepting composite or S-Video input) and a TV capture card (also only accepting composite or S-Video input). AVGA does work with VGA monitors, but I'd like to keep the hardware interface simple - just two resistors, ideally.


In the end, I ended up writing my own library. It currently has the following specifications:
32x16 characters: 512 bytes (half of the total SRAM on the ATmega168) are used to store the text buffer. Full 256 characters at a resolution of 6x8 pixels each. Total screen resolution: 192x128. The library is interrupt-driven, and uses the sixteen-bit TIMER1 to schedule events. This means that the AVR is only busy generating video signals when it absolutely has to, leaving some CPU time to the user program. When outputting at full quality, the AVR appears to be capable of running user code at 3.3 MIPS, but by skipping alternate scanlines (each scanline is scanned twice anyway, so this mainly just makes the display appear darker) the AVR appears to be running user code at 9.9 MIPS. (I say "appears" as my calculation has been to execute a busy loop that would normally take one second on the AVR running at its normal 20 MIPS then seeing how long it takes with the video output driver enabled).

">
tvText demo on YouTube
The above video demonstrates some of the currently rather limited features of the library. The text console handles a subset of the BBC Micro VDU commands - I'd like to support as many of its features as possible. The code behind the BASIC-like part of the demo is simply written like this:

#include "tvtext/tvtext.h"

void type_string_P(const char* s) {
char c;
while ((c = pgm_read_byte(s++))) {
tvtext_putc(c);
delay_ms(100);
}
}

int main(void) {

tvtext_init();

tvtext_clear();
tvtext_puts_P(PSTR("AVR Computer 1K\r\n\nATmega 168\r\n\nBASIC\r\n\n>"));
delay_ms(2000);
type_string_P(PSTR("10 PRINT \"AVR Rules! \";\r\n"));
tvtext_putc('>');
delay_ms(500);
type_string_P(PSTR("20 GOTO 10\r\n"));
tvtext_putc('>');
delay_ms(500);
type_string_P(PSTR("RUN"));
delay_ms(1000);
tvtext_puts_P(PSTR("\r\n"));

for (int i = 0; i 200; ++i) {
tvtext_puts_P(PSTR("AVR Rules! "));
delay_ms(20);
}

tvtext_puts_P(PSTR("\r\nEscape at line 10\r\n>"));
delay_ms(1000);
type_string_P(PSTR("CHAIN \"DEMO\""));
delay_ms(1000);

// ...

}

All of the high-level console code - text output, viewport scrolling, cursor positioning &c - has been written in C, so should be relatively easy to be customised. The output driver itself has been written in assembly as timing is critically important.

With a few more features and a bit of tidying up I hope that people would find this a useful library. I'd certainly like to get a blinking cursor working within the driver, and if I add support for a reduced 128-character version I could save quite a bit of ROM space and add support for "coloured" - inverted, that is - text. NTSC support would also be quite useful.

benryves

benryves

 

64-bit IThumbnailProvider, BBC BASIC matrices and clocks

Work commitments have left me with little time to pursue my own projects of late, hence the lack of updates.

A chap named Jeremy contacted me with problems relating to the IThumbnailProvider code I'd posted here before. We narrowed it down to a 64-bit issue, demonstrated by the fact that the thumbnails appeared in the file open dialog of a 32-bit application, but not in Explorer. Not having a 64-bit version of Windows to tinker with, I was unable to help, but he found the solution was to register the assembly using the 64-bit version of regasm. You can read more about his experiences on his blog.

I had made a mistake in the BBC BASIC (Z80) for TI-83+ documentation, describing the old coordinate system in the graphics documentation rather than the current one (which is more closely aligned to other versions of BBC BASIC). I have uploaded a new version of the documentation to ticalc.org. This build also includes some basic matrix operations via the MAT statement. This statement is rather incomplete, but I've run out of ROM space (not to mention time) to implement it fully. Still, the bits that are there are quite useful, and a half-arsed implementation is better than no implementation... right?


On a whim, I purchased a 32x8 LED display on eBay which I've (very) slowly been turning into a remote-controlled clock. A Sony-compatible remote control is used to type in the time, after which you can cycle through different styles with the channel up/down buttons and change the brightness with the volume and mute buttons. I'm using a 4MHz PIC16F84 to drive the display, with a DS1307 responsible for time-keeping and a 32KB 24LC256 to store the font data and text strings.

As well as dates and times, I thought a thermometer might be a useful addition so I put together an order for a DS18B20. It's silly to just order one thing, so I bulked up the order with one of the snazzy new PICAXE-20X2 chips (yes, they run a BASIC interpreter but the new 64MHz clock speed is certainly impressive). I find PICAXE microcontrollers invaluable for prototyping, being so very easy to use! [smile]

In an attempt to broaden my horizons, I also purchased two AVRs, as I have zero experience with these popular chips. I went for the two ends of the scale as offered by the supplier - an ATmega168 and an ATtiny13. Having lost a battle with PayPal's cart (it kept forgetting old items as I added new ones) I neglected to purchase a D-sub hood so I'll be waiting until I can go and buy one before I start assembling a programmer. I was intending on going for the simple SI Prog, but if anyone has any suggestions for variations I'd be interested in hearing them!

benryves

benryves

 

Cogwheel 1.0.3.0 beta 3

I managed to break save states in the last build of Cogwheel (attempting to load a save state would fail, not being able to set a property). I've marked the offending read-only property with [StateNotSaved] and made the loader slightly more robust in Cogwheel 1.0.3.0 beta 3. It's beta 3, not 2, because I uploaded 2 and then noticed another issue - you couldn't change the controller mappings! This is something that must have been broken for ages, but either nobody noticed or they just didn't care to report it. Oh well, that's been fixed now. For some reason Google don't let you re-upload files, so beta 3 it has to be.


Another addition is this build is preliminary support for persistent cartridge RAM. Some games, such as Phantasy Star (pictured above) let you save your progress in the game onto battery-backed RAM built into the cartridge. If you come back to the game later you should now be able to continue your progress without needing to manually save the entire emulator state.

I've had reports of rather bizarre crashes bringing one poor user's machine to its knees. I'm at a loss to establish why; I've tried the emulator on four machines (two Vista, two XP) and although one of the machines displays a white screen instead of the emulator output (no pixel shader 2.0 support on its Radeon 9000) the software trundles along just fine otherwise (I can at least hear the game music!) The one notable difference between my machines and his machine is that he's using a 64-bit version of Windows, and all of the ones I have access to run 32-bit Windows. To see if this is the issue, I've changed the configuration to x86 (I've encountered strange bugs with .NET code using unmanaged 32-bit code on 64-bit Windows) to see if this will remedy issues, but if anyone has any bright ideas I'd be interested to hear them.

benryves

benryves

 

Cogwheel 1.0.3.0 beta 1

I've released a beta version of Cogwheel 1.0.3.0 in the hope of getting some feedback. My main concern is with the new 3D glasses code, so I'd be very grateful if you could install the emulator and run this ROM in it. The ROM simply alternates between showing a red screen for the left eye and a cyan one for the right eye (the emulator defaults to only showing the left eye, so you'll just see red for the moment). If you select a different 3D glasses mode (Options, 3D glasses, Row interleaved) you should end up with something like this:



If you drag the around the desktop the lines should appear fixed on the spot (as long as you drag it slowly enough to allow it to repaint), and if you resize it the entire form should always be covered in lines one pixel apart. The same should apply to the column interleaved and chequerboard interleaved modes.

I've also added VGM recording and VGM playback. VGM playback is handled by bundling Maxim's SMS VGM player.


The console's region (Japanese or Export) and video standard (NTSC or PAL) are now user-configurable via the Emulation menu. The YM2413 (FM sound) emulation has been converted to straight C# (it used to be a P/Invoked native DLL). Drag-and-drop support has been added to aid in loading ROMs, save-states and VGMs.

There have been a number of internal optimisations, fixes and tweaks (such as per-scanline backdrop colours), but nothing too major (compatibility is roughly the same as it was). If you do find any bugs, please report them!

benryves

benryves

 

New 3D renderer in Cogwheel

I have written a new 3D-compatible renderer for Cogwheel. It holds two textures, one for each eye, and uses one of a number of different effect file techniques to mix the two views.


Based on the interlacing work from the previous entry, the first technique is one that uses interleaved rows. I'm not really sure if there's a good way to convert texture coordinates into device coordinates, so am passing in the viewport height as a parameter and hoping that floating point errors don't trip me up (they haven't, yet).

float4 RowInterleavedPixelShader(VertexPositionTexture input) : COLOR0 {
float row = input.Texture.y * ViewportHeight * 0.5f;
if (abs(round(row) - row) 0.1f) {
return tex2D(LeftEyeSampler, input.Texture);
} else {
return tex2D(RightEyeSampler, input.Texture);
}
}

Alternate pixel centres may also pose a problem in the future. If anyone had any recommendations, suggestions or warnings on the way I'm detecting the evenness or oddness of a particular "scanline" then I'd appreciate hearing them!


I have also added two other interleaving modes; one in columns and another in a chequerboard pattern. I included these two as I've seen that some 3D LCD panels use a column interleaving pattern (I suppose that with a lenticular lens in front of such a panel you may not even need 3D glasses) and apparently Sharp have displays that use the chequerboard pattern.

I have also taken advantage of pixel shaders to create colour and monochrome anaglyphs (previously calculated in software), though neither look as good as the above full-colour modes for shutter glasses or similar hardware.

There are a few issues I need to sort out first before I can release this; for example, there's no way to set whether the first row/column/pixel is for the left or right eye. More problematic is the removal of support for non power-of-two textures; the Master System's 256x192 display is fine, but the Game Gear's 160x144 display gets rounded up to 192 pixels wide (and yes, I know that's not a power of two) on my video card. I also mean to give Promit's SlimTune profiler a look to see if I can optimise some of the less efficient pieces of my code. The C# version of emu2413 is probably a good candidate, being a "dumb" translation from the original macro-heavy C.

benryves

benryves

 

Taking advantage of interlacing for 3D

To achieve smooth, glitch-free 3D in an ideal world, one would like to be able to alternate between left and right eye views every time the monitor refreshes. You could then use the monitor's vertical synchronisation pulse to alternate which eye shutter is currently open.

Relying on software is not so bad if you can guarantee that you will be able to keep up with the video hardware and output alternating frames without dropping any. This is pretty much impossible with today's complex operating systems running any number of background tasks that could interfere with your render loop at any moment.

Fortunately, video cards already have a mode that is guaranteed to output a different image every frame - interlaced scan. Rather than send each scanline (row) of the image every frame ("progressive scan"), it alternates between sending every even-numbered scanline and every odd-numbered scanline. This halves the vertical resolution, but allows you to double the refresh rate using the same amount of bandwidth.


We can take advantage of this scan mode to encode the left and right views into a single image. The view for the left eye is stored in the even-numbered scanlines and the view for the right eye is stored in the odd-numbered scanlines, as in the image above. When displayed on a monitor using interlaced scan, it appears as the following:


As the video card takes care of alternating which set of scanlines (or field) is displayed, the result is that the left and right views alternate perfectly in time with the monitor's refresh rate.

To test this, I've attached a counter IC to the vertical synchronisation pulse of a VGA monitor. The low bit of the counter toggles every time the screen refreshes, and this is used to select which eye shutter is open on the 3D glasses. The glasses are driven using the serial port adaptor described in the previous post.


The result is perfectly stable 3D images. The circuit does not know which field, and hence which eye's view, is visible at any time - it just alternates left and right, which means that there is a 50% chance that the left and right views are flipped. This can be fixed by switching the circuit off then on again to try and resynchronise, but a better solution would be to add a switch to toggle the uncovered eye manually and to fix the synchronisation. As this would only need to be done once per session this isn't much of an issue! An alternative fix would be to add a switch to switch the left and right views in software.

The logic probe in the above photo is an integral part of the design - at least, I assume it is, as if I remove it the circuit doesn't work correctly! I assume there's a noise issue that's interfering with the circuit (none of the unused inputs on the counter chip are tied low) and the logic probe contains some noise suppression circuitry of its own.

The only real fly in the ointment here is video driver support for interlaced scan modes. My video card only supports 1920x1080 at 30i, 29i and 25i, and only if the primary monitor is an LCD. I can work around the problem by cloning the primary LCD to the CRT and setting both to 1920x1080 at 30i, but my LCD displays a warning message and makes a distressing noise and 30i, whilst faithful to the Master System's 30fps 3D, is unpleasantly flickery. It would be wonderful if I could drop the resolution down a bit and switch to 60i, but I can't see a way to do that with ATi's drivers.

benryves

benryves

 

3D LCD Shutter Glasses Experimentation

The Sega Master System supported 3D LCD shutter glasses to provide a more immersive (if somewhat flickery) playing experience. Having caught wind of an eBay member selling compatible glasses for $9 and being rather interested in stereoscopy I decided to experiment a little for myself.


LCD shutter glasses with adaptor
The glasses are pretty simple; they consist of two LCD panels that can be "switched on" to block light from passing through to each eye. A 3.5mm stereo jack plug provides the electrical connection.

To display the 3D image you alternate between showing the image for the left eye and the right eye on the monitor, uncovering the corresponding eye immediately before the image appears on the monitor. This effectively halves the refresh rate (and results in fairly noticeable flicker when run at the standard NTSC 60Hz) and prevents the 3D glasses from working with displays that respond too slowly (eg LCD panels). I've had to dig out my old CRT monitor for this project. Even if my LCD did refresh quickly enough, its polarisation is perpendicular to the polarisation of the shutter glasses, meaning that no light can pass from the LCD through the glasses even when both eye LCD panels are switched off.


Adaptor insides
The adaptor I'm using is based on this circuit (I'm using the second variation with the variable resistor to fine-tune the driving frequency). The LCD panels require an AC voltage, and using a EOR gate as an oscillator allows the whole device to be constructed out of a single IC with a handful of external components. More importantly, being based on an existing and public design allows me to ensure that any work I do should be compatible with adaptors other people have built.

The DTR pin on the serial port supplies power to the circuit and the RTS pin is used to choose which LCD panel is switched on to cover an eye.


Test pattern seen through glasses
The above image displays a test pattern viewed through the glasses. The software alternates between clearing the screen to red with the left eye shutter open and clearing the screen to cyan with the right eye shutter open. The colours were deliberately chosen to match the colours of the common anaglyph glasses. As the colours are alternated very quickly (in the interests of avoiding a headache I used a refresh rate of 120Hz) the screen appears a light grey colour to the naked eye.

Most LCD shutter glasses appear to use some form of feedback from the video signal to synchronise the alternating of eyes. On a PC they could alternate every time the vsync signal appeared on the VGA port, on a TV they could open the correct eye shutter depending on the current field of the interlaced image that was being displayed. The Sega Master System's video chip can generate an interrupt when entering the vblank period which you can use to prepare the next frame and update the glasses. An adaptor connected to the PC's serial port has no such luck - I have not yet found a way to reliably synchronise the glasses to the refresh rate.


Poor synchronisation (and even worse photography) results in images like the above, as seen through the left shutter. As the LCD shutters have been updated too late, some of the display intended for the right eye is seen through the left eye at the top of the display - the cyan band in this case.

So far, the best result I've had is to use Direct3DEx's DeviceEx, which provides a handy WaitForVBlank method. Less handy is the Vista requirement, as is the slight delay between returning from this function and updating the serial port, resulting in a flickering band near the top of the display as per the previous photograph. For the best results I need to set the presentation interval to "immediate", which compounds the issue with occasional tearing caused by the delay between WaitForVBlank returning and calling Present.

Switching the presentation interval back to "1" (tying the refresh rate of the render loop to that of the monitor) results in complete frames (no tearing or bands of the wrong colour/image), but the additional delay before presenting the next frame puts updating the LCD glasses out of sync by one frame. As the uncovered eye should alternate between subsequent frames one can simply uncover the "opposite" eye to uncover the correct image, but any dropped frames throw this out of sync and you get the occasional "inside out" view when the wrong eye is uncovered. Any background tasks on the PC kicking in could potentially cause a dropped frame. This is one reason that a VGA pass-through adaptor that automatically alternates the uncovered eye every frame wouldn't work, as it would get thrown out of sync by these dropped frames.


A demo compatible with the Sega 3D glasses, showing the images for each eye as stereo pair
The advantage of using an existing adaptor design makes me reluctant to pursue solutions that involve additional hardware to fix the problem. One possible solution I can think of would be an additional pass-through box that contains a simple latch that is clocked by the VGA vsync signal between the serial port and the glasses adaptor. You could then set the state of the glasses immediately before calling Present, safe in the knowledge that your signal will only get through to the adaptor box perfectly synchronised with the CRT's vertical refresh, assuming the CRT doesn't enter vblank between updating the serial port and calling Present. Not having to manually synchronise to vsync in software would remove the need for the Vista-only Direct3DEx too.

benryves

benryves

 

RC-5, NEC, JVC and Panasonic infrared codes

I've rewritten the remote control signal decoding software to handle multiple protocols. As well as SIRCS, it now supports RC-5, NEC, JVC and two Panasonic codes (one "old" 11-bit code and one "new" 48-bit code). There's not much in the way of screenshots at the moment, other than a debug window that gets filled when keys are pressed:
Gripping stuff, I'm sure you'll agree.

The C# source code for this can be downloaded here.

A keyring remote control (courtesy of Poundland) has highlighted one possible issue in handling repeating buttons. Rather than target any particular device, it will try and brute-force a response. For example, here's the result of pressing the power button once in one particular mode:

That's four different protocols from one button. I suppose some sort of mapping from protocol-specific code to a string (so those five commands would be translated into five "power" strings) and comparing the time between signals to turn the input into something meaningful may help, but that would require an enormous database of known codes.

benryves

benryves

 

Remote controlling Windows the Sony way

It's been a while since I last posted, and unfortunately this post is to do with Sony remote controls again. [rolleyes]

This time I'm attempting to use Sony (or compatible) remote controls to control software running on a Windows PC. I've recently been watching more films in PowerDVD, and some of the keyboard shortcuts (eg Ctrl+P for the menu) are a little difficult to hit in the dark and from a distance. I have a ready supply of universal remote controls as well as the PlayStation 2 DVD remote control, all of which work with the SIRCS protocol.


Serial port infra-red receiver built into an old TI GraphLink cable
First up is the required hardware. This involves an infrared demodulator connected to a free serial port. I chose the serial port as .NET provides a way to handle pin change events and you do not need administrator rights to access it (as per the parallel port). I also had a broken Texas Instruments GraphLink cable that could be ripped apart to act as a case.


Infra-red receiver module schematic
The circuit is pretty simple. Pin 4 (DTR) and 5 (GND) from the serial port form the power supply. DTR can be set to either +12V or -12V, so a rectifier diode is used to keep the input voltage above 0V. Following that is a reverse-biased zener diode and resistor to regulate the voltage below 5.1V. Finally, the output pin of the infra-red demodulator is connected to the input pin 8 (CTS) of the serial port.


Infra-red receiver module assembled on stripboard
The software handles the SerialPort.PinChanged event to time the length of input pulses. Once it detects a start bit (2.4mS) it starts decoding the rest of the command. When it's finished receiving a command it fires an event of its own, which the main software can react to.

using System;
using System.Diagnostics;
using System.IO.Ports;

namespace BeeDevelopment.Sircs {

///
/// Represents a command sent by a SIRCS remote control.
///
public struct SircsCommand : IEquatable {

#region Properties

private byte command;
///
/// Gets or sets the command value.
///
public byte Command {
get { return this.command; }
set { this.command = value; }
}

private short device;
///
/// Gets or sets the device identifier.
///
public short Device {
get { return this.device; }
set { this.device = value; }
}

private int length;
///
/// Gets or sets the length of the command in bits.
///
public int Length {
get { return this.length; }
set { this.length = value; }
}

#endregion

#region Construction

///
/// Creates an instance of a structure.
///
/// The command value.
/// The device identifier.
/// The length of the command in bits.
public SircsCommand(byte command, short device, int length) {
this.command = command;
this.device = device;
this.length = length;
}

#endregion

#region Methods

///
/// Converts the into a string.
///
/// A string representation of the .
public override string ToString() {
return string.Format("Command={0:X2}, Device={1:X4}, Length={2}", this.command, this.device, this.length);
}

///
/// Returns the hash code for this instance.
///
/// The hash code for this instance.
public override int GetHashCode() {
return this.command ^ this.device ^ this.length;
}

///
/// Returns a value indicating whether this instance is equal to another instance.
///
/// The instance to compare to this one for equality.
/// True if the instances are equal, false otherwise.
public bool Equals(SircsCommand other) {
return this.command == other.command && this.device == other.device && this.length == other.length;
}

///
/// Returns a value indicating whether this instance is equal to another instance.
///
/// The instance to compare to this one for equality.
/// True if the instances are equal, false otherwise.
public override bool Equals(object other) {
return other != null && other is SircsCommand && ((SircsCommand)other).Equals(this);
}

#endregion

#region Operators

public static bool operator ==(SircsCommand a, SircsCommand b) { return a.Equals(b); }
public static bool operator !=(SircsCommand a, SircsCommand b) { return !a.Equals(b); }

#endregion

}

#region Events

///
/// Represents the method that will handle the SircsCommandReceived event.
///
/// The object that fired the event.
/// Information about the event.
public delegate void SircsCommandReceivedEventHandler(object sender, SircsCommandReceivedEventArgs e);

///
/// Provides data for the SircsReceived.SircsCommandReceived event.
///
public class SircsCommandReceivedEventArgs : EventArgs {

#region Properties

///
/// Gets the that was received.
///
public SircsCommand Command { get; private set; }

///
/// Gets the number of times that the incoming command has been repeated when held.
///
public int Repeat { get; private set; }

#endregion

#region Construction

///
/// Creates a instance.
///
/// The that was recieved.
/// The number of times that the incoming command has been repeated when held.
public SircsCommandReceivedEventArgs(SircsCommand command, int repeat) {
this.Command = command;
this.Repeat = repeat;
}

#endregion

#region Methods

///
/// Converts the into a string.
///
/// A string representation of the .
public override string ToString() {
return string.Format("{0}, Repeat={1}", this.Command, this.Repeat);
}

#endregion
}

#endregion

///
/// Provides a way to receive SIRCS commands from a simple receiver attached to a serial port.
///
public class SircsReceiver : IDisposable {

#region Constants

///
/// The minimum time length for a start bit (nominally 2.4ms).
///
private const double StartBitMinLength = 2.0E-3;

///
/// Threshold time length between a "low" (0.6ms) and a "high" (1.2ms) bit.
///
private const double DataBitLengthThreshold = 0.9E-3;

///
/// The maximum time length between data bits. If this is exceeded, any data command transfer is cancelled.
///
private const double IntraBitMaxLength = 0.8E-3;

///
/// The maximum time length between repeating commands. Commands are supposed to repeat every 45ms.
///
private const double RepeatCommandMaxLength = 120.0E-3;

#endregion

#region Private Fields

///
/// The that the receiver is connected to.
///
private SerialPort Port = null;

///
/// The last time that the pin state changed in ticks.
///
private long LastPinChangedTime = 0;

///
/// A instance used to time incoming bits.
///
private Stopwatch BitTimer = null;

///
/// Set to true when receiving a command, false otherwise.
///
private bool ReceivingCommand = false;

///
/// Counts the number of bits currently received.
///
private int BitsReceived = 0;

///
/// Stores the command as it gets built up.
///
private uint Command = 0;

///
/// Stores the last received command.
///
private SircsCommand LastCommand = default(SircsCommand);

///
/// Stores the number of times the received command has been repeated.
///
private int LastCommandRepeatCount = 0;

///
/// A instance used to time repeating commands.
///
private Stopwatch RepeatTimer = null;

#endregion

#region Construction/Destruction

///
/// Creates an instance of a from a serial port name.
///
/// The name of the serial port the receiver is connected to.
public SircsReceiver(string portName) {

// Set up the serial port.
this.Port = new SerialPort(portName);
this.Port.PinChanged += new SerialPinChangedEventHandler(PinChanged);

// Open the port for access.
this.Port.Open();
this.Port.DtrEnable = true;
this.Port.RtsEnable = true;

// Get the timers running.
this.BitTimer = new Stopwatch();
this.BitTimer.Start();
this.RepeatTimer = new Stopwatch();
this.RepeatTimer.Start();
}

///
/// Releases the resources used by this instance.
///
public void Dispose() {
if (this.Port != null) {
this.Port.PinChanged -= new SerialPinChangedEventHandler(PinChanged);
this.Port.Dispose();
this.Port = null;
}
}

~SircsReceiver() {
this.Dispose();
}

#endregion

#region Events

///
/// An event that is fired when a is received.
///
public event SircsCommandReceivedEventHandler SircsCommandReceived;

///
/// A method that is invoked when a is received.
///
///
protected virtual void OnSircsCommandReceived(SircsCommandReceivedEventArgs e) {
if (this.SircsCommandReceived != null) this.SircsCommandReceived(this, e);
}

#endregion

#region SIRCS protocol handling

void PinChanged(object sender, SerialPinChangedEventArgs e) {

// Respond to changes on the CTS pin.
if (e.EventType == SerialPinChange.CtsChanged) {

// Quickly grab the current time and current CTS level.
long CurrentPinChangedTime = this.BitTimer.ElapsedTicks;
bool CurrentLevel = this.Port.CtsHolding;

// Calculate the time elapsed.
long DeltaTime = CurrentPinChangedTime - this.LastPinChangedTime;
double SecondsElapsed = (double)DeltaTime / (double)Stopwatch.Frequency;
this.LastPinChangedTime = CurrentPinChangedTime;

if (CurrentLevel) { // If the current signal level is high, we may assume that we've just timed a low pulse.

// Have we received a start bit?
if (SecondsElapsed > SircsReceiver.StartBitMinLength) {
this.ReceivingCommand = true;
this.BitsReceived = 0;
this.Command = 0;
} else if (this.ReceivingCommand) {
// Process incoming bit.
this.Command >>= 1;
if (SecondsElapsed > SircsReceiver.DataBitLengthThreshold) {
this.Command |= unchecked((uint)(1 31));
}

// Have we received enough bits?
switch (++this.BitsReceived) {
case 12:
case 15:
case 20:
// We've received enough bits to handle the input as a received command.
// Check to see if there's any more data forthcoming.
long EndTime = CurrentPinChangedTime + (long)(Stopwatch.Frequency * SircsReceiver.IntraBitMaxLength);
while (BitTimer.ElapsedTicks if (!(CurrentLevel = this.Port.CtsHolding)) break;
}
// The input is still high - there's no more data coming in; we've received a command.
if (CurrentLevel) {
// Construct a struct to hold information about the recieved data.
SircsCommand ReceivedCommand = new SircsCommand(
(byte)((this.Command >> (32 - this.BitsReceived)) & 0x7F),
(short)(this.Command >> ((32 + 7) - this.BitsReceived)),
this.BitsReceived
);

// Reset the timer.
this.ReceivingCommand = false;
this.BitTimer.Reset();
this.BitTimer.Start();
this.LastPinChangedTime = 0;

// Calculate the repeat count.

// Quickly grab the current time and current CTS level.
long RepeatTimeTicks = this.RepeatTimer.ElapsedTicks;
this.RepeatTimer.Reset();
this.RepeatTimer.Start();

// Calculate the repeat time elapsed.
double RepeatTimeSeconds = (double)RepeatTimeTicks / (double)Stopwatch.Frequency;

// Is the command repeating?
if (ReceivedCommand == this.LastCommand && RepeatTimeSeconds ++this.LastCommandRepeatCount;
} else {
this.LastCommandRepeatCount = 1;
this.LastCommand = ReceivedCommand;
}

// Fire the event.
this.OnSircsCommandReceived(new SircsCommandReceivedEventArgs(ReceivedCommand, this.LastCommandRepeatCount));
}
break;
}
}
} else { // If the current signal level is low, we may assume that we've just timed a high pulse.
// If a high pulse is too long, cancel any incoming commands.
if (SecondsElapsed > SircsReceiver.IntraBitMaxLength) {
this.ReceivingCommand = false;
this.BitTimer.Reset();
this.BitTimer.Start();
this.LastPinChangedTime = 0;
}
}
}
}

#endregion

}
}

Currently, the software reacts to input events by running through a list of scripts, passing the command ID, device ID and command length (in bits) to each until one of them returns zero (ie, success) to indicate that it has processed the button.


Scripts list
The advantage to this method is that the end-user could customise the behaviour of the software to their own liking very easily. For example, here's the PowerDVD.js file from above, which allows me to control PowerDVD from a PlayStation 2 DVD remote control:

// Table of commands.
var Commands = [
{ Command : 0x00, Device : 0x093A, Length : 20, Shortcut : '1' }, // 1
{ Command : 0x01, Device : 0x093A, Length : 20, Shortcut : '2' }, // 2
{ Command : 0x02, Device : 0x093A, Length : 20, Shortcut : '3' }, // 3
{ Command : 0x03, Device : 0x093A, Length : 20, Shortcut : '4' }, // 4
{ Command : 0x04, Device : 0x093A, Length : 20, Shortcut : '5' }, // 5
{ Command : 0x05, Device : 0x093A, Length : 20, Shortcut : '6' }, // 6
{ Command : 0x06, Device : 0x093A, Length : 20, Shortcut : '7' }, // 7
{ Command : 0x07, Device : 0x093A, Length : 20, Shortcut : '8' }, // 8
{ Command : 0x08, Device : 0x093A, Length : 20, Shortcut : '9' }, // 9
{ Command : 0x09, Device : 0x093A, Length : 20, Shortcut : '0' }, // 0
{ Command : 0x0B, Device : 0x093A, Length : 20, Shortcut : '{ENTER}' }, // Enter
{ Command : 0x0E, Device : 0x093A, Length : 20, Shortcut : '{ESC}' }, // Return
{ Command : 0x1A, Device : 0x093A, Length : 20, Shortcut : 'lt' }, // Title
{ Command : 0x2A, Device : 0x093A, Length : 20, Shortcut : 'x' }, // AB
{ Command : 0x28, Device : 0x093A, Length : 20, Shortcut : 'd' }, // Time
{ Command : 0x2C, Device : 0x093A, Length : 20, Shortcut : '^r' }, // Repeat
{ Command : 0x30, Device : 0x093A, Length : 20, Shortcut : 'p' }, // Previous
{ Command : 0x31, Device : 0x093A, Length : 20, Shortcut : 'n' }, // Next
{ Command : 0x32, Device : 0x093A, Length : 20, Shortcut : '{ENTER}' }, // Play
{ Command : 0x33, Device : 0x093A, Length : 20, Shortcut : 'b' }, // Scan
{ Command : 0x34, Device : 0x093A, Length : 20, Shortcut : 'f' }, // Scan >>
{ Command : 0x38, Device : 0x093A, Length : 20, Shortcut : 's' }, // Stop
{ Command : 0x39, Device : 0x093A, Length : 20, Shortcut : ' ' }, // Pause
{ Command : 0x54, Device : 0x093A, Length : 20, Shortcut : 'z' }, // Display
{ Command : 0x60, Device : 0x093A, Length : 20, Shortcut : '^b' }, // Slow
{ Command : 0x61, Device : 0x093A, Length : 20, Shortcut : 't' }, // Slow >>
{ Command : 0x63, Device : 0x093A, Length : 20, Shortcut : 'u' }, // Subtitle
{ Command : 0x64, Device : 0x093A, Length : 20, Shortcut : 'h' }, // Audio
{ Command : 0x65, Device : 0x093A, Length : 20, Shortcut : 'a' }, // Angle
{ Command : 0x79, Device : 0x093A, Length : 20, Shortcut : '{UP}' }, // Up
{ Command : 0x7A, Device : 0x093A, Length : 20, Shortcut : '{DOWN}' }, // Down
{ Command : 0x7B, Device : 0x093A, Length : 20, Shortcut : '{LEFT}' }, // Left
{ Command : 0x7C, Device : 0x093A, Length : 20, Shortcut : '{RIGHT}' }, // Right
];

// Search for the matching command.
var Command = null;
for (var enumerator = new Enumerator(Commands); !enumerator.atEnd(); enumerator.moveNext()) {
var TestCommand = enumerator.item();
if (TestCommand.Command == WScript.Arguments(1) && TestCommand.Device == WScript.Arguments(2) && TestCommand.Length == WScript.Arguments(3)) {
Command = TestCommand;
break;
}
}

// No command.
if (!Command) WScript.Quit(1);

// Find the PowerDVD process ID.
var PowerDvdId = null;
var WmiService = GetObject('winmgmts://./root/cimv2');
var Processes = WmiService.ExecQuery('Select ProcessId From Win32_Process Where Name="PowerDVD.exe"');
for (var enumerator = new Enumerator(Processes); !enumerator.atEnd(); enumerator.moveNext()) {
PowerDvdId = enumerator.item().ProcessId;
break;
}

// If we haven't found the process ID, quit with an error.
if (!PowerDvdId) WScript.Quit(1);

// Activate the PowerDVD instance.
var WshShell = new ActiveXObject('WScript.Shell');
WshShell.AppActivate(PowerDvdId);

// Send the shortcut keys.
WshShell.SendKeys(Command.Shortcut);

WScript.Quit(0);

Unfortunately, this method has quite a lot of overhead. This becomes a problem when you consider that commands are repeated every 45ms. Currently I avoid the issue by not allowing any keys to repeat, but some keys - such as the volume keys - would need to repeat when held.

I'm unsure as the best path to take. One idea that has crossed my mind would be to set up each remote control you were going to use beforehand (though I suppose I could build up a database of remote controls and bundle them with the software). You could then set whether each key should repeat or not, and attach a meaningful string to each button. This would also allow for more protocols to be supported other than SIRCS, and you could set it up so that the Play button on a Sony remote control generated the string "play" and passed that to the script(s) as well as the Play button on a Panasonic or Toshiba remote control rather than juggling control codes.

benryves

benryves

 

Decoding SIRCS commands with a PIC16F84

Some time ago I was working on a simple Z80-based computer. It has a PS/2 keyboard and mouse port for user input, and these are implemented using a large number of discrete parts - transistor drivers with all manner of supporting latches and buffers. The AT protocol (which the PS/2 keyboard and mouse inherit) is entirely implemented in software by the Z80.

On the one hand this design has a certain purity, but it ties the CPU up every time data is to be transferred. The keyboard sends data when it feels like it, so if you wished to perform some function based on a key press event you'd need to poll the port periodically, assuming that if communications time out there's no key waiting. All this hanging around does nothing good for performance.

As it turns out I found a PIC16F84 in an old school project over the weekend, so downloaded its datasheet and the MPLAB IDE and tried to puzzle it out.

The 16F84 is a pretty venerable microcontroller with a 1K flash memory for program code, 68 bytes of data RAM and 64 bytes of data EEPROM. It can run at up to 10MHz, and is based on a high-performance RISC CPU design. It has 13 digital I/O pins, each of which can be configured individually as either an input or an output. I'm well aware there are far better microcontrollers around these days, but this one was just sitting around doing nothing.


Click to toggle labels
Above is the circuit I constructed to work with the 16F84. The HRM538BB5100 in the top-right is an infrared demodulator and amplifier module; it will output 5V until it receives a 38kHz infrared signal (such as the one emitted by most remote controls) at which point it outputs 0V. By timing the lengths of the IR pulses one could decode a remote control signal, and that's the aim of this project - decode a command from a Sony remote control and display it on the two 7-segment displays. The 10MHz crystal is probably overkill for this simple task, but it's the slowest I had available!

In fact, the 10MHz crystal works out quite neatly. Most instructions execute in one instruction cycle, which is four clock cycles. Four clock cycles at 10MHz is 400nS. The 16F84 has an internal timer that counts up after every instruction cycle and triggers an interrupt when it overflows from 255 back to 0; 400nS*256=102.4us. If we call that 100us (close enough for jazz) then it overflows 10 times every millisecond. The SIRCS protocol is based around multiples of 0.6ms, which makes this rate very easy to work with.

; ========================================================================== ;
; Pins: ;
; RB0~RB6: Connected to A~G on the two seven-segment displays. ;
; RB7: Connected via a 220R resistor to cathode of the left display. ;
; Inverted and connected via a 220R resistor to right display's ;
; cathode. ;
; RA0: Connected to the output of the HRM538BB5100. ;
; ========================================================================== ;

#include

list p=16F84

__CONFIG _CP_OFF & _WDT_OFF & _PWRTE_ON & _HS_OSC

; ========================================================================== ;
; Variables ;
; ========================================================================== ;
udata
IsrW res 1 ; Temporary storage used to preserve state during the
IsrStatus res 1 ; interrupt service routine.

Display res 1 ; Value shown on 7-segment displays.

PulseTimer res 1 ; Counter to time the length of pulses.

BitCounter res 1 ; Number of bits being received.
Command res 1 ; SIRCS command.

; ========================================================================== ;
; Reset ;
; ========================================================================== ;
ResetVector code 0x0000
goto Main

; ========================================================================== ;
; Interrupt Service Routine ;
; ========================================================================== ;
ISR code 0x0004

; Preserve W and STATUS.
movwf IsrW
swapf STATUS,w
movwf IsrStatus

; Update value shown on two 7-segment displays.
movfw Display
btfsc PORTB,7
swapf Display,w
andlw h'F'
call Get7SegBits
btfss PORTB,7
xorlw b'10000000'
movwf PORTB

; Increment pulse timer.
incfsz PulseTimer,w
movwf PulseTimer

; Acknowledge timer interrupt.
bcf INTCON,T0IF

; Restore W and STATUS.
swapf IsrStatus,w
movwf STATUS
swapf IsrW,f
swapf IsrW,w
retfie

; ========================================================================== ;
; Times the length of a "low" pulse. ;
; ========================================================================== ;
; Out: W - Length of pulse. ;
; ========================================================================== ;
TimeLow
clrf PulseTimer
TimeLow.Wait
btfsc PORTA,0
goto TimeLow.GoneHigh
incfsz PulseTimer,w
goto TimeLow.Wait
TimeLow.GoneHigh
movfw PulseTimer
return

; ========================================================================== ;
; Times the length of a "high" pulse. ;
; ========================================================================== ;
; Out: W - Length of pulse. ;
; ========================================================================== ;
TimeHigh
clrf PulseTimer
TimeHigh.Wait
btfss PORTA,0
goto TimeHigh.GoneLow
incfsz PulseTimer,w
goto TimeHigh.Wait
TimeHigh.GoneLow
movfw PulseTimer
return

; ========================================================================== ;
; Convert a hex nybble (0-F) into a format that can be displayed on a 7-seg ;
; display. ;
; ========================================================================== ;
; In: W. Out: W. ;
; ========================================================================== ;
Get7SegBits
addwf PCL, f
dt b'00111111' ; 0
dt b'00000110' ; 1
dt b'01011011' ; 2
dt b'01001111' ; 3
dt b'01100110' ; 4
dt b'01101101' ; 5
dt b'01111101' ; 6
dt b'00000111' ; 7
dt b'01111111' ; 8
dt b'01101111' ; 9
dt b'01110111' ; A
dt b'01111100' ; b
dt b'00111001' ; C
dt b'01011110' ; d
dt b'01111001' ; E
dt b'01110001' ; F

; ========================================================================== ;
; Start of the main program. ;
; ========================================================================== ;
Main

; Set PORTB to be an output.
bsf STATUS,RP0
clrw
movwf TRISB
bcf STATUS,RP0

; Configure TMR0.
bsf STATUS,RP0
bcf OPTION_REG,T0CS ; Use internal instruction counter.
bcf STATUS,RP0

; Enable TMR0 interrupt.
bsf INTCON,T0IE
bsf INTCON,GIE

clrf Display

; ========================================================================== ;
; Main program loop. ;
; ========================================================================== ;
Loop

WaitCommand
; Loop around waiting for a low to indicate incoming data.
btfsc PORTA,0
goto WaitCommand

; Start bit (2.4mS).
call TimeLow
; Check that it's > 2mS long.
sublw d'20'
btfsc STATUS,C
goto WaitCommand ; w20

; Reset the command variable and get ready to read 7 bits.
clrf Command
movlw d'7'
movwf BitCounter

ReceiveBit
; Time the pause; should be call TimeHigh
sublw d'10'
btfss STATUS,C
goto WaitCommand

; Time the input bit (0.6ms = low, 1.2ms = high).
call TimeLow
sublw d'9'
; Shift into the command bit.
rrf Command,f

decfsz BitCounter,f
goto ReceiveBit

bsf STATUS,C
rrf Command,f
comf Command,f

movfw Command
movwf Display

goto Loop

; ========================================================================== ;
; Fin. ;
; ========================================================================== ;
end

The final source code is above. I'm not sure how well-written it is, but it works; pointing a Sony remote control at the receiver and pressing a button changes the value shown on the seven-segment display. PICmicro assembly is going to get take a little getting used to; instructions are ordered "backwards" to the Intel order I'm used to (op source,destination instead of the more familiar op destination,source) and as far as I can tell literals default to being interpreted as hexadecimal as opposed to decimal.

With some luck I can now teach the 16F84 the AT protocol and replace a large number of parts on the Z80 computer project with a single IC. It does feel a little like cheating, though!

benryves

benryves

 

Expression Evaluation in Z80 Assembly

The expression evaluators I've written in the past have been memory hungry and complex. Reading the BBC BASIC ROM user's guide introduced me to the concept of expression evaluation using top-down analysis, which only uses a small amount of constant RAM and the stack.

I took some time out over the weekend to write an expression evaluator in Z80 assembly using this technique. It can take an expression in the form of a NUL-terminated string, like this:

.db "(-8>>2)+ceil(pi())+200E-2**sqrt(abs((~(2&4)>>>(30^sin(rad(90))))-(10>?1)))",0

and produce a single answer (or an error!) in the form of a floating-point number. The source code and some notes can be downloaded here.

I initially wrote a simple evaluator using 32-bit integers. I supported the operations the 8-bit Z80 could do relatively easily (addition, subtraction, shifts and logical operations) and got as far as 32-bit multiplication before deciding to use BBC BASIC's floating-point maths package instead. The downside is that BBC BASIC has to be installed (the program searches for the application and calls its FPP routine).

I'm not sure if the technique used is obvious (I'd never thought of it) but it works well enough and the Z80 code should be easy to follow - someone may find it useful. [smile]

benryves

benryves

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!