Albireo - Programmer's manual

Getting started

This is the reference for the Albireo board. It is a technical document to provide early adopters with all the information they need to make use of the device and write software for it. Hopefully the said software will come with a more easily understandable manual.

Complete feature list

The board is quite feature packed. Here is an attempt to list everything.

Software support

These can be used with Albireo: Of course you can also write your own software.

I/O ports

Albireo is largely built around two independant chips, the CH376 and the TL16C550D. Because of cost and chip count limitations in address decoding, they each have slightly separate address ranges. The decoding is clean, this means there aren't any mirror ports or undecoded address bits. Just the addresses listed below are used.

The addresses are in the I/O range, which means you access them with the OUT and IN instructions. They are not memory mapped, and to match with the CPC address decoding, the address is decoded on 16 bytes. This makes using OTIR and similar looped instructions tricky, but is required for compatibility with the CPC.

CH376 registers

TL16C550D registers

Some of the registers are sharing the same address. A register bit (DLAB) is used to switch between the two groups.

AddressDLABDescription
FEB00RBR/THR: RX buffer (read), TX buffer (write)
FEB10IER: Interrupt enable
FEB01DLL: Divisor latch LSB
FEB11DLM: Divisor latch MSB
FEB2IIR/FCR: Interrupt status (read), FIFO control (write)
FEB3LCR: Line control
FEB4MCR: Modem control
FEB5LSR: Line status
FEB6MSR: Modem status
FEB7SCR: Scratch register

DIP switches

The board holds 4 DIP switches for configuration. From top to bottom:

S1 - CH376 interrupt enable

When this switch is ON, the usb controller is allowed to generate interrupts to signal the CPC when it is done performing an operation.

When this switch is OFF, the usb controller is not allowed to generate interrupts. The CPC must then poll the CH376 STATUS register to know wether the operation is finished.

S2 - CH376 reset enable

When this switch is ON, the CH376 will be reset at the same time as the CPC (hardware reset only). When this switch is OFF, the CH376 will not be reset, and the CPC must initialize it using the reset command (software reset). In this case, the CH376 internal buffer may be used to store reset resident data (but I don't know if this is of any practical use, yet).

S3 - Remote reset enable

When this switch is ON, the DTR signal from the remote side of the serial link is connected to the CPC reset. This means that the remote side computer can trigger the CPC reset by toggling that line. When the switch is OFF, such reset is not allowed and the CPC is safe.

S4 - Remote interrupt enable

When this switch is ON, the DTR signal from the remote side of the serial link is plugged to the DSR line of the UART controller. It then generates an interrupt which the CPC can process.

Detecting the hardware

Detecting the CH376

You can use the CHECK_EXIST command for this. It gets one byte as argument, and returns the negation of it.

CH376_CMD	EQU	0xFE81
CH376_DATA	EQU	0xFE80

CMD_CHECK_EXIST	EQU	0X06

		; Send the command
		LD	BC,	CH376_CMD
		LD	E,	CMD_CHECK_EXIST
		OUT	(C),	E

		; Send one data byte
		LD	BC,	CH376_DATA
		OUT	(C),	E

		; Get the result
		IN	A,	(C)

		; Check that the result is the same as what we sent
		XOR	E
		INC	A
		JR	NC,	ERROR_NOT_DETECTED

		; Here, we know that the CH376 is present!

Detecting the TL16C550D

The T16C550D can be detected using its scratch register. This is a read/write register with no effect on the chip. We can check that writing a value there and reading it back works.

SCR	EQU	0xFEB7

	LD	BC,	SCRATCH
	LD	A,	0x55
	OUT	(C),	A
	IN	A,	(C)
	CP	0x55
	JP	NZ,	ERROR_NOT_DETECTED

	; Here, we know the TL16C550D is present!

Interrupts

All interrupts on the board are handled by the TL16C550D. They can be configured to trigger either an INT or an NMI, or nothing at all.

Individual interrupts control

The TL16C550D manages several different interrupt sources. These can be individually configured using the interrupt enable register.

Interrupt routing

The interrupts can also be routed to NMI or INT, or none of them if you don't want your code to be interrupted. This is done using the OUT1 and OUT2 bits of the modem control register (MCR).

Interrupt identification

When an interrupt occurs, the first step in the interrupt handler is to identify where it comes from. This board provides support only for Z80 interrupt mode 1.

You get information about interrupts by first reading the IIR register. If bit 0 is set, it means Albireo is not the one which generated the interrupt. You should turn to other hardware plugged to your CPC, or, if all else fails, to the gate array interrupt.

If the bit is cleared, there is a pending interrupt from Albireo. You then look at bits 1-3 of IIR to determine which interrupt it is.

External interrupts

The identification of the external interrupts in the MSR is as follows:

Double acknowledge of interrupts

Due to the way the interrupts are wired, the TL16C550D will trigger an interrupt both when the CH376 triggers one, but also when it is cleared by software. This means after clearing the CH376 interrupt, the interrupt flags of the TL16C550D should be checked again to make sure there isn't any other pending interrupt.

This is not a problem when using the INT pin, because everything will happen with interrupts masked by the z80. However, when using the NMI, there is no masking and this would trigger a new entry into the NMI routine, which is probably not what you want.

To avoid this problem in NMI mode, it is recommended that the NMI handler masks the NMI by using the OUT2 bit in MCR, while it processes the interrupt. This allows to perform all tasks as required. Once all interrupts have been cleared (IIR bit 0 is set again), it is safe to enable the NMI again.

The same applies for the remote control interrupt, when clearing it on the remote side a new interrupt will be triggered. A similar approach can be used: mask out the NMI, tell the remote side the interrupt is handled and let it clear the bit, acknowledge that with the TL16C550D, and finally enable the NMI again.

The CURSOR interrupt is not subject to this, because it is edge triggered. You can use it as an NMI source without such problems. The interrupt is cleared simply by reading the MSR register.

Using USB devices

The documentation for the CH376 chip is only partially available in english. Pretty much the only thing you can get is CH376DS1.pdf, which is only the first part of the datasheet. It covers the use with mass storage drives, only. However, the CH376 can also do raw USB transfers, and even act as an USB device. Getting this to work requires digging into WCH original documents, which are all in chinese. It is possible to get away with that thanks to Google Translate or other similar tools. If all else fails, it's also possible to ask for help on WCH forums, which are mainly in chinese, but they did reply to my questions in english. Using the CH376 in this way requires some understanding of how USB works in general. I recommend reading "USB in a nutshell" and "USB made simple" (both available freely online) in order to get basic understanding of how things are laid out.

General information about the CH376 and its family

The CH376 is a chip manufactured by the chinese company WCH. It is an upgrade of their previous CH372 (USB device only) and CH375 (USB host, mass storage without filesystem management). As far as I can tell, all 3 devices are essentially rebadged 8051 microcontrollers. What makes them nice for us, is the availability of an 8-bit parallel interface, which is easy to interface to any CPU, and the tolerance for 5V TTL I/O without any problems. What makes them less nice is that most of the documentation is available only in chinese, making it a bit difficult to locate and use the many available resources. But, this page is here to fix that. The CH376 can only do USB1. This limits the speed to 12 megabytes per second, which is more than enough in our case (the CPC isn't fast enough to use it to its full potential). There is a newer chip, the CH378, which brings USB2 support. The microcontrollers used to implement all this are also sold separately. But, as far as I could see, the firmware to implement the complete CH376 functions is not available. Possibly, parts of it such as the 8-bit bus interface, are available.

CH376 I/O interface

The CH376 is interfaced to the z80 bus at ports FE80 and FE81 (using 16-bit addresses in the IO space as usual on CPC). FE80 is the data port, and FE81 is the command and status port. The usual command execution goes this way: There are variants. Some commands complete immediately and an interrupt is not required. Some do not affect the status byte.

The status register

Only 2 bits are of interest in the status register in our case.

Complete list of commands handled by the CH376

You would think a single datasheet would provide a complete list of commands. But, that is not the case. WCH decided to scatter the command list in different documents, depending on when they were introduced (some commands were for the CH372, more were added for the CH375, and even more for the CH376), and to which functionality group they belong to (mass storage, raw USB host, raw USB device).

01 - GET_IC_NUMBER

This function returns the chip firmware version number. All chips I've dealt with used firmware version 4.

02 - SET_BAUDRATE

This is used to configure the baud rate for the serial interface. This is not of any use to us, as we use the parallel interface and there is no notion of a baudrate there.

03 - ENTER_SLEEP

Put the chip in low-power mode.

05 - RESET

Hardware reet of the CH376. This can take up to 35 milliseconds. It is recommended to do this for proper initialization of the chip.

06 - CHECK_EXIST

This command expects 1 byte written to the data register. It will return the 1-complement (byte XOR 0xFF) in the data register. This can be used to detect the CH376 chip presence.

0B - SET_SD0_INT

Configure interrupts when using the SPI interface. This is not relevant for us, as we use the parallel interface.

0C - GET_FILE_SIZE

Returns the size of the file currently open.

15 - SET_USB_MODE

Switch between different operation modes of the CH376.

22 - GET_STATUS

Get the CH376 status. This function is used to check the result of all asynchronous commands. The typical flow is: The command returns one byte in the DATA register.

27 - RD_USB_DATA0

This command is used to read data returned by USB operations. The typical flow is: The command returns first the number of bytes, then the data itself, in the DATA register. (to be continued)

Available resources at WCH

Practical case: driver for an HID mouse

Since the early developments on Albireo, things have changed a little in the CPC world and several other mass storage interfaces are now easily available. However, another problem was still not completely solved: there was not a simple solution for connecting a mouse to the CPC.

Of course, the Albireo USB port can drive any USB peripheral, including any standard USB mouse. It is no more needed to dig out a PS/2 compatible mouse for your Symbiface, or even worse, an Atari/Amiga one for the MutliPlay.

Moreover, the USB standard provides a rather high level view on events from the mouse, which makes implementing this with low CPU use possible (the mouse driver was initially written completely in BASIC, and if using assembler, an interrupt driven setup is possible, freeing most CPU cycles).

This also serves as an example of how USB communications are implemented with the CH376. It opens the possibility of writing drivers for more devices. Gamepads, printers, webcams... you name it.

The code in this section is written in BASIC-like syntax. This makes it easy to read for everyone.

Initialization

First of all, we need to initialize and detect the CH376. We start by sending it the RESET command:

OUT CMD,&05

This can take up to 35 milliseconds to complete, so we wait a little. Next, we try to detect if the CH376 is up and running. We use the CHECK_EXIST command for that.

OUT CMD,&06
OUT DAT,&55
v = INP(DAT)

If the value of v is &AA, we have detected the CH376. If it is not, something went wrong. The detection doesn't always work reliably, so you may want to try a few times just to be sure.

We can additionnally check the CH376 version:

OUT CMD,&01
v = INP(DAT) - &40

In all Albireo boards currently available, you should get v = 4.

Device enumeration

Now that the CH376 is ready and awaiting orders, we can enable its USB port. Note that switching the mode will disconnect it from any USB or SD mass storage, if that is also used. Switching between the two dynamically may require some experimenting.

Anyway, let's switch our USB device on!

OUT CMD,&15:OUT DAT,7:v = INP(DAT)
OUT CMD,&15;OUT DAT,7:v = INP(DAT)

This sequence puts the CH376 in "HOST" mode. In this mode it acts just like a PC and you can connect USB devices to it.

HID mouses are slow-speed devices. We must force the CH376 to use low speed.

OUT CMD,&0B:OUT DAT,&17:OUT DAT,&D8

This command acts like a "POKE" into the CH376 internal memory. It would be cleaner to use the SET_SPEED command, but the example I got from WCH forums uses this, and it works.

The next step is to assign our USB device with its own address. When they are newly connected to a computer, all USB devices initially get an address of 0. It is up to the host to scan them and assign them other addresses. This should allow to use multiple devices at the same time, but I haven't tried that yet.

OUT CMD,&45:OUT DAT,1:v = INP(DAT)

So, we just tell our device to use address 1. And we must also tell the CH376 itself that it should now communicate with device 1, which is a separate command:

OUT CMD,&13:OUT DAT,1

The device may expose several "configurations", that is, several different ways to talk to it. In the case of our mouse, we will just try the first configuration.

OUT CMD,&49:OUT DAT,1

This command generates an interrupt. If you have interrupts enabled, you can wait for that, otherwise just busy loop on the status register:

WAIT:

v = INP(CMD)
IF v > 127 THEN GOTO WAIT

OUT CMD,&22
STATUS = INP(DAT)
RETURN

This little subroutine waits for an interrupt and then reads and return the status (using CMD_GET_STATUS). Most of the time the status should be &14, when everything went well.

Configuring the mouse

Ok, the device and configuration are now set, which means we are now ready to communicate with the mouse. We are taking some shortcuts here and assuming the user did in fact plug a mouse and not some other device. Ideally, we should get the device descriptor and see what kind of device it is at this point.

Since we are only interested in the mouse, we can send it some HID commands. HID (Human Interface Device) is a part of the USB standard, and tells how to communicate with input peripherals: mouses, keyboards, gamepads, tablets, etc.

For this simple test, we will request the mouse to use "boot mode". This is a simplified mode which is designed for use in PC BIOSes. It removes some of the complexity of HID. The drawback is, it supports only 3 buttons and 2 movement axes. Basically, that means no mouse wheel.

It is possible to use the mouse in standard ("report") mode, but doing this properly is a little more complex, and there may be more compatibility problems with different mouses.

So, let's first tell the CH376 we want to perform a data transfer:

OUT CMD,&2C

And let's tell how many bytes there is:

OUT DAT,8

The CH376 is now waiting for our 8 data bytes. We will now write the HID command to enter boot mode:

OUT DAT,&21 ' (because all HID commands start with &21)
OUT DAT,&0B ' (this is the HID SET PROTOCOL command)
OUT DAT,&00:OUT DAT,&00 ' (protocol number 0 - on 16 bits - is the BOOT protocol)
OUT DAT,&00:OUT DAT,&00:OUT DAT,&00:OUT DAT,&00

All USB commands must also include an index and a data length, 16 bits each. Both are set to 0 for the SET PROTOCOL command.

The bytes are now loaded in the CH376. We can request it to perform the USB transaction:

OUT CMD,&4E:OUT DAT,&80:OUT DAT,&0D

Command 4E (ISSUE_TOKEN_X) is the command used to trigger data transfers with the USB devices.

The second parameter tells we are performing a control transfer (D), on endpoint 0 (the 4 high bits). An USB device has several endpoints, which are like independant communication channels. Endpoint 0 is used for control transfers, specific commands to configure the device.

GOSUB WAIT

We need to wait for the transfer to complete. When it does, our mouse will be ready for reporting data. Before entering the main loop, we need an important initialization:

TOKEN = 0

Reading mouse data

The mouse, like any HID device, sends its data on endpoint 1 which is an "interrupt" endpoint. In USB terms, that means... we must poll for data on the endpoint regularly. Quite the opposite of an interrupt, if you ask me.

So, let's ask the mouse for this data. This time it is a READ transaction, so we start with ISSUE_TOKEN_X:

LOOP:

OUT CMD,&4E:OUT DAT,TOKEN:OUT DAT,&19

As you can see, this is a READ transaction (9), and it happens on endpoint 1 (4 high bits of the last parameter).

We need to prepare our token for the next time:

TOKEN = TOKEN XOR &80

The token is used by the CH376 to decide wether the data it will send or receive should be a DATA0 or DATA1 packet. On a given USB endpoints, there should always be an alternance of DATA0 and DATA1 packets. If two consecutive data packets have the same type, the second one will be rejected (with a STALL). So, each time command 4E is used, the token should alternate between 00 and 80.

GOSUB WAIT

We now need to wait for the mouse to reply to our request. Note that the mouse will reply with a NAK if it has nothing to say, however in that case the CH376 will keep retrying, until the mouse says something. There are two options to handle this: go doing something else instead of waiting, and look at the status byte from time to time to see if there are some changes, or configure the CH376 to not retry on NAKs. I leave this as an exercise for the reader.

Once the mouse moves or a button is clicked, it will send an HID report. The status bit (and the interrupt pin, if configured) signals an interrupt, and we can proceed with reading the mouse state:

OUT CMD,&27
LENGTH = INP(DAT)

Command 27 allows us to read the data from the CH376 internal buffer. It first returns the nulber of bytes received. We ignore this because we are in a simple example and we know the HID mouse in boot mode always sends data in the same format. But, a complete program should check it nonetheless.

We can finally read the data:

BUTTONS = INP(DAT)
X = INP(DAT)
Y = INP(DAT)

We get the buttons state (lsb is the left button, 2nd bit is the right one, 3rd bit is the middle one), and the X and Y deltas since the last report as signed 8-bit values.

GOTO LOOP

And that's it! We can send the command to get the next report and start over.

Other resources

Test program I wrote to check that the Albireo boards are working fine:

The datasheets of the chips used on the board may be useful: