Foreword
In the previous post we added joystick support to our C64 FPGA from the Linux operating system. In addition we expanded the system to be able to use either Joystick Port #1 or Joystick Port #2.In this post we will I show how to initialise the sound chip on the Zybo board from Linux, so that we can hear the sounds from the SID module.
Speaking of the sound system. A couple of posts ago I mentioned that I have updated my Github repo with the recent changes of our C64 core module, excluding the sound system.
I am pleased to announce that my Github repo now also contains the addition of the sound system.
Rendering SID samples to a speaker
Some time ago, I wote two posts, here and here, where I explained how to incorporate Thomas Kindler's SID core into our C64 design.My discussion in these two posts basically stopped at the point where the SID samples was serialised over the I2S bus. I haven't explained the supporting processes at all that needs to happen so that the sounds finally gets rendered on a speaker.
Having an overview of these processes is necessary to understand the steps required for initialising the Sound System. So, let's get started!
Sound is produced and captured on the Zybo board by means of the SSM2603 chip from ANALOG Devices.
This chip receives sound samples via the I2S bus and is configured via a I2C bus. As previously mentioned we have already implemented the I2S bus for sending Audio Samples.
We haven't, however, discussed the implementation to interface with the I2C bus. Luckily one doesn't need to implement a I2C module from scratch, since the Zynq contains two I2C onchip peripherals.
In the next section I will briefly highlight what is required to use one of these onchip I2C peripherals to initialise the SSM2603.
The question is, however, how do we use a onchip I2C pheriperhal in our design? We will cover this in the next section.
Using a onchip I2C peripheral
To surface a I2C peripheral in our design, we need to configure our Zynq block. So, start by double clikcing on the Zynq Block, selecting the MIO Configuration section and opening I/O peripherals.There is a couple of things you should do here. First you need to select the option I2C 1. With this option selected a drop down will appear next to this option in the IO column.
In this dropdown in the IO column you need to select to which external pins on the ZYNQ this I2C peripheral should be attached to. The first couple of options are MIO pins. If we select any of these we will not see the I2C pins in our block design at all. The only option that will surface the pins in our design is EMIO:
With this option selected the I2C pins will now appear within our design in the Zynq block:
You will see that I have already linked up these pins to the rest of the diagram. the iobuf block is a custom block I have created have pin as a birectional pin. The direction of this pin is controlled by the tristate input.
This is all there is to wiring up these pins. Next, let us see what software changes is required to drive these pins and to ultimately initialising the SSM2603.
Initialising SSM2603 from Linux
When I originally developed the SID functionality in the C64 core, I initialised the SSM2603 in a Bare-Metal application. Here is the source for the Bare-Metal application: https://github.com/ovalcode/c64fpga/blob/master/SDK.src/standalone/c64.cThere is quite a bit going on in this application and apart from initialising the SSM2603, we are also reading from a USB keyboard, as explained in a previous post. The key method to look for is init_sound().
If you follow the code in init_sound(), you will see that we are directly writing to the registers associated with the I2C 1 controller. Since we are working in Linux, at the moment, it is probably better practice to see if one can open I2C 1 as a device file and then manipulate the SSM2602 with this file.
In approaching this problem with accessing I2C 1 as a device file, I found myself trying to fiddle with the device tree to try and enable the I2C driver. This ended up been quite a mission, so I reverted, at least for now, to copy the register writes/read from the standalone application and just modifying it a bit, so it can work in Linux. The whole SSM2603 init sequence we will also make part of the Kernel driver we have been developing in the last couple of posts.
As we have seen in previous posts, when you want to access physical memory locations in Linux, you first need to map it to a virtual address space. So, let us do it for the I2C 1 registers:
static void __iomem *i2c_reg; static int __init ebbchar_init(void){ ... c64_reg = ioremap(0x43c00000, 16000); c64_reg = c64_reg; c64_reg_screen_mode = c64_reg + 8; c64_reg_keyboard_0 = c64_reg; c64_reg_keyboard_1 = c64_reg + 4; tape_mem_area = ioremap(0x1f500000, 2000000); i2c_reg = ioremap(0xE0005000, 128); ... }
The address returned by ioremap is an address in virtual address space. From this point onward when you want to access of the I2C registers, you need to use i2c_reg as your base and then add your register offset.
So, for instance, if you want to write the value 0x1f to the register 0x1c, you will do the following:
iowrite32(0x1f, i2c_reg+0x1c);
Similarly, if you want to read a register, you will do something like the following:
status = ioread32(i2c_reg+0x10) & 1;
You will see that in the standalone code, we are using two different operators for doing a read and write from a register: Xil_In32 and Xil_Out32. So when using this code in Linux remember to convert it to ioread32 and iowrite32 respectively. Also, remember that the parameter order of iowrite32 is different than that Xil_Out32: In Linux it is value followed by address and Xil_Out32 starts with the address, followed by the value.
If you want find the final source for the Linux Kernel driver, just go here: https://github.com/ovalcode/c64fpga/tree/master/SDK.src/linux
Another fine difference between the standalone code and the Linux code, is the operator we use for introducing a delay. In the standalone code I use usleep, where the value should be microseconds. Also, in Linux I am using msleep, where the value should be in milliseconds.
This is about all the changes required to initialise the SSM2603 from Linux.
When equivalent isn't exactly equivalent
In the previous post I have basically copy and pasted the SSM2603 initialisation code to our Linux Kernel driver, with minor changes. So, in theory, this code should just worked in our Linux Driver.However, when I tried this code for the first time, the SSM2603 didn't initialise at all.
After some investigation, I found that the driver got stuck in the following loop within the method writeReg:
do { status = ioread32(i2c_reg+0x10) & 1; } while (!status);
This loop checks one of the status bits of I2C 1, which is set as soon as the I2C have shifted out a chunk of information.
This really confused me, because the equivalent code on the Bare-Metal application worked perfectly. I tried thinking of a couple of possibilities that was causing this.
Firstly I wanted to know if the pins from I2C really got routed to EMIO instead of one or other MIO pin.
So armed, with the XSCT console, I inspected the memory locations for MIO routing and comparing the same registers to when Linux was running.
Maybe I should just take a step back and give an some background to the checks I was doing.
The MIO registers provides you with some options to route the I2C pins to different external pins. As an example, please have a look on page 1643 of the Zynq Technical Reference manual.
This register gives you the option to route the Serial Clock pin of the I2C 1 to pin 12. Similar registers exists, like 0xF8000734 and 0xF8000740, which allows you to route I2C pins to other MIO pins.
When I inspect these registers when running the Bare-Metal application, I found that no MIO pin is configured for surfacing any I2C 1 pin. This is exactly what I expect.
When inspecting the same registers when running Linux, I got the same result.
This inspection let to a bit of a dead-end, but there is still a question remaining: How do enable pins to get routed through EMIO?
At first sight I couldn't find any information about this in the Zynq TRM. However, the diagram on page 49 provided some subtle information for me.
As seen on that diagram, all peripherals go into the MIO, which performs the necessary multiplexing as describe earlier.
Some of these peripherals are also connected to the EMIO. From the diagram it looks like the connections to EMIO are direct, so in theory these pins should always be available to the FPGA.
Once again I reached a dead-end. What else could be the reason why the the SSM2603 doesn't initialise in Linux?
I did a further search in the Zynq TRM PDF to see if I could locate any other registers that is related to the I2C peripheral, and eventually I came across the register APER_CLK_CTRL on page 1586. Specifically, my eyes ctached the following phrase:
Please note that these clocks must be enabled if you want to read from the peripheral register space.This could be indeed the problem, we are trying to read the status, but we are not getting any sensible data back, because the AMBA clock is not enabled for I2C 1. For clocking to be enabled for I2C 1, bit 19 should be set for register APER_CLK_CTRL.
I could confirm that this bit was set when the Bare-Metal application was running, and, as a relief, I could confirm that bit wasn't set when running Linux!
This is luckily an easy fix!
... static void __iomem *clk_reg; ... static int __init ebbchar_init(void){ ... clk_reg = ioremap(0xF8000000, 0x200); ... iowrite32(0x01EC044D, clk_reg+0x12c); msleep(2); init_sound(); ... } ...
I introduced a small delay after setting the bit in the register APER_CLK_CTRL.
After the fix the SSM2603 initialisation worked in Linux. One can quickly detect that the initialisation is working by hearing the speaker attached to Zybo board turning on.
In Summary
In this post we looked into how to initialise the SSM2603 for sound in Linux.This set of blog posts on how to create a C64 on a Zybo is quickly running to an end, since I have achieved more or less all my goals with it.
That been said, there is so much more you can use the Zybo board for, some of these of which I also want to write Blog posts about.
I also want to write some posts about using the C64 outside of a FPGA context, like writing a Chess Engine.
So, in coming posts I will introduce some variety, that might not be related to implementing a C64 on on FPGA.
I might, however, revist the topic of C64 on an FPGA from time to time, for instance implementing a 1541 running in parallel with the C64 in the FPGA.
Till next time!