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.
The board is quite feature packed. Here is an attempt to list everything.
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.
Some of the registers are sharing the same address. A register bit (DLAB) is used to switch between the two groups.
|FEB0||0||RBR/THR: RX buffer (read), TX buffer (write)|
|FEB1||0||IER: Interrupt enable|
|FEB0||1||DLL: Divisor latch LSB|
|FEB1||1||DLM: Divisor latch MSB|
|FEB2||IIR/FCR: Interrupt status (read), FIFO control (write)|
|FEB3||LCR: Line control|
|FEB4||MCR: Modem control|
|FEB5||LSR: Line status|
|FEB6||MSR: Modem status|
|FEB7||SCR: Scratch register|
The board holds 4 DIP switches for configuration. From top to bottom:
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.
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).
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.
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.
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!
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!
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.
The TL16C550D manages several different interrupt sources. These can be individually configured using the interrupt enable register.
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).
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.
The identification of the external interrupts in the MSR is as follows:
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.
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.
First of all, we need to initialize and detect the CH376. We start by sending it the RESET command:
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.
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.
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:
And let's tell how many bytes there is:
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.
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
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.
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.
And that's it! We can send the command to get the next report and start over.
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: