I2C FM Radio - Part 15 Microcontroller Basics (PIC10F200)New

Published


Hi again! After a relatively long break, I want to continue with the tutorials about the PIC10F200 microcontroller. This time, let’s learn about how to make an FM radio. You can ask: how is microcontroller and an FM radio related? The answer is: directly! The technology isn’t always an all-in-one device, though the whole FM radio can be located in a single tiny chip to which you just need to connect the antenna, the earphones, and the… microcontroller, which controls the operation of this chip. If this sounds interesting for you, then welcome to this tutorial.

First, I want to warn you that the tutorial will be very meaty. It will include a description of the I2C (inter-integrated circuit) interface, because the FM chip uses it. Then we’ll consider the FM chip itself, and also an external EEPROM memory chip which we will use to store some radio settings.

So, let’s start!

The I2C interface is very widespread in the microcontrollers world. It’s mainly used to communicate between the microcontroller and some peripheral devices, like external memory, RTC (real-time clock), ADC (analog to digital converter), DAC (digital to analog converter), OLED displays, I/O expanders, etc.

Its popularity is based on several certain advantages:

1) It requires just two wires to communicate - SCL (serial clock) and SDA (serial data), see figure 1.

Figure 1. Connection of two devices via I2C interface.
Figure 1. Connection of two devices via I2C interface.

2) Ability to connect up to 127 devices to the same wires in parallel which allows you to use just two pins of the microcontroller to control a lot of devices (figure 2).

Figure 2. Parallel connection of several devices.
Figure 2. Parallel connection of several devices.

3) Simplicity of the software implementation of the interface, you can implement it using even a very primitive microcontroller without any problems.

Soon, we will verify these points in practice.

I2C has two nominal communication speeds - normal (100 kHz) and fast (400 kHz) so it mainly is used for relatively slow devices.

I2C also has a strict hierarchy - usually there is one Master device which initiates the communication, and several Slave devices which only listen to the Master and do what it says. There can be a multi-master implementation of I2C but we will not consider it in this tutorial.

As you may notice from figure 1 and figure 2 there are pull-up resistors R1 and R2 on both lines - SCL and SDA. These resistors are mandatory and should be always installed. Inside the devices, the SCL and SDA pins have transistors with open collectors. So when you want to transmit ‘1’ to the line, you just close the transistor, and the line is automatically pulled up by the external resistor. And if you want to transmit ‘0’, you open the transistor, and it ties the line to the ground. This makes the communication safe, as you will not short-circuit your lines even if one device transmits ‘1’ and another transmits ‘0’ to the same line.

In the normal state all output transistors are closed, and both lines are tied to VCC with the pull-up resistor. But how does the Master device start the communication? To answer this question we need to consider the I2C protocol logic. The standard I2C packet is shown in figure 3. (Image is taken from Easyelectronics.ru)

Figure 3. I2C packet.
Figure 3. I2C packet.

The communication always starts with the Start condition (left red area in figure 3). This is a special condition when the SDA line goes low while the SCL line remains high. Take care if you implement your I2C interface in software to not to change data bits while SCL is high, as this will be recognized as a Start condition.

The Start condition is followed by eight data bits which go MSB first. As you can see in figure 3, all changes of the SDA line are performed when SCL is low during transmission of the byte.

After the byte is transmitted/received, the Master performs one more extra pulse as a so-called “Acknowledgement bit” (A bit in figure 3). This bit is used to indicate if the recipient has received the byte correctly. If it has, then it sets the SDA line low during the acknowledgement bit (line in figure 3). If a device is absent or something goes wrong, then Not-acknowledgement is transmitted, which is represented by keeping SDA high.

After the acknowledgement bit, the Stop condition is issued (right red area in figure 3). This is another special condition when SDA goes high while SCL remains high. After that, communication is considered as finished, and the bus is released.

We’ve just considered the physical implementation of the I2C interface, now let’s see how it’s implemented in the logic level. There can be two main cases: master transmits data to the slave, and master reads data from the slave. Let’s see how the first one is done. (figure 4, taken from i2c.info)

Figure 4. Master transmits data to Slave.
Figure 4. Master transmits data to Slave.

As I’ve already mentioned, there can be up to 127 devices in the I2C bus connected in parallel. To distinguish them, each one should have a unique address. In the standard I2C specification the addresses are 7-bit length (that’s why the limitation is 127 devices - 2^7). After issuing the Start condition the Master transmits the 7-bit address of the device to which it wants to send the data. This address is followed by the R/W bit. If this bit is 0, then the Master says that it wants to transmit the data, and if it’s 1 the Master wants to receive data from the Slave. In our case this bit is 0 (figure 4). As all the devices are connected in parallel, they all receive this packet from the Master and compare it with their own address. If any of them has the address that has been transmitted, it issues the acknowledgement (ACK) after the address byte. If none of the devices have the transmitted address, then SDA will remain high, and Master will receive Not-acknowledgement (NACK), after which it should issue the Stop condition to release the bus. If everything’s OK, and ACK is received, then the Master can transmit the data without needing to issue a new Stop-Start condition. Each data byte should be followed by the ACK from the Slave (figure 4). When the last byte is transmitted, the Master issues the Stop condition and releases the bus.

Let’s now consider the situation when the Master expects data from the Slave (figure 5, taken from i2c.info).

Figure 5. Master received data from Slave.
Figure 5. Master received data from Slave.

In this case the Master transmits the Slave’s address followed by the R/W bit set to 1. After that the Slave device should send ACK, and then the data byte, after which the Master should send the ACK to the Slave. Then it will send the next byte, and so on. If the Master needs no more data, it sends NACK after the last byte and issues the Stop condition (figure 5). This last NACK is very important, as some devices may keep the SDA line low until they receive NACK from the Master, and not allowing the Stop condition and tying up the communication lines, so don’t forget about this.

The last thing I want to mention is a special condition, called “Repeated Start” (see figure 6, taken from i2c.info)

Figure 6. Repeated start condition.
Figure 6. Repeated start condition.

I wouldn’t mention it to avoid overloading your mind with unnecessary details, but unfortunately (or fortunately) we will use this condition in our program. It’s usually used if we communicate with the same device and want to switch from transmitting data to receiving. In this case, we issue the first Start condition, send the slave’s address with the R/W bit reset to 0, then send some data, and then instead of issuing Stop, we issue Start again, and send the Slave address with the R/W bit set to 1 to read some data from it.

Well, I think that’s enough I2C theory to understand what will happen in our program. If you want to know more, you can read the articles mentioned above in Wikipedia or i2c.info or any other resource - there are plenty of them on the Internet.

And we will consider the new chips that will be used to build our FM radio.

First, the RDA5807M module (figure 7).

Figure 7. RDA5807M module.
Figure 7. RDA5807M module.

This module is a complete FM radio system. It can receive the signal, decode it, convert it into the stereo sound, and output to your earphones. The output is weak enough, so if you want to connect to a speaker, you’d better use some additional power amplifier. It also can decode the RDS/RBDS data, but as we don’t have a display in our device, we will not consider this ability. As usual, you can read the full information in the datasheet. Despite its high functionality, the price of the module starts at $0.35 at AliExpress.

The pinout of the module is shown in figure 8.

Figure 8. RDA5807M module pinout.
Figure 8. RDA5807M module pinout.

As you see, connection is quite simple, the power is applied through VCC and GND pins.

Warning! The supply voltage of this chip is 1.8…3.3 V, so please be careful and don’t use 5V power, otherwise you can burn the chip.

SDA and SCL pins are used to control the chip via the I2C bus. Lout and Rout are sound outputs of the left and right channels respectively. And Antenna is the antenna input, just connect a piece of wire to it. The length of the wire should be aliquot to the wave length to achieve the best sensitivity. For 100 MHz the wave length is about 3m. So 1.5m or 75 cm length will be fine.

The module itself is quite small, and the pitch is just 2mm, so you need to have some practice in soldering to use it.

The module is controlled by reading and writing its registers via the I2C bus. There are a lot of registers (see the datasheet) but we will use just three of them, and not even completely. Each register is 16-bit length, so to write to it, you need to transmit the higher byte first, and then the lower byte.

Let’s consider these registers:

  • Register 0x02:
    • bit 15 - DHIZ (Audio Output High-Z Disable). Should be set to 1 to enable the audio output
    • bit 14 - DMUTE (Mute Disable). Should be set to 1 to unmute the radio (it’s muted after powering up)
    • bit 13 - MONO. Forces the radio to the mono mode if set to 1. It’s 0 by default.
    • bit 12 - BASS (Bass Boost). Speaks for itself. If set to 1 boosts the bass. It’s 0 by default
    • bits 11 and 10 are 0 by default and need not to be changed.
    • bit 9 - SEEKUP (Seek Up). Sets the direction of searching the station. If it’s 0 then search performs from higher frequency to lower, and vice versa if it’s 1.
    • bit 8 - SEEK. When you set this bit to 1, the module automatically starts to search the station in the direction determined by the SEEKUP bit. When the station is found, this bit is reset automatically.
    • bits 7 to 1 are 0 by default and need not to be changed.
    • bit 0 - ENABLE (Power Up Enable). If this bit is 1 then the module is enabled and operates in a normal mode, and if it’s 0, then the module goes into power-down mode (it’s 0 by default).
  • Register 0x03:
    • bits 15-6 - CHAN (Channel Select). These bits represent the current frequency on which the radio is tuned. You can write these bits to set the frequency manually, but to have an effect, you also need to set the TUNE bit of this register.
    • bit 5 is 0 by default and need not to be changed.
    • bit 4 - TUNE, when you set this bit to 1, the radio starts to tune to the frequency specified with the CHAN bits. When the tuning process is finished, this bit is reset.
    • bits 3 to 0 are 0 by default and do not need to be changed.
  • Register 0x05:
    • bits 15 to 4 are 0b100010001000 = 0x888 by default and don’t need to be changed.
    • bits 3 to 0 - VOLUME, sets the volume of the sound, 1111 - maximum, 0000 - minimum value.

The interesting thing is that this chip has two (actually three but we’ll ignore that) I2C addresses. First is 0x20/0x21. By this address the sequential mode is supported. The writing starts with the register 0x02, then the register address is automatically incremented. And the reading starts with the register 0x0A which we didn’t consider, and won’t use in our device.

The second address is 0x22/0x23. By this address random access is supported. When sent, it should be followed by the register address you want to read/write.

That’s all we need to know about the RDA5870M module to use it in our FM radio.

To make the operation of the radio more comfortable, it would be good to store the current volume level and a current station in the non-volatile memory. Unfortunately, PIC10F200 is so primitive that it neither has an in-built EEPROM nor is able to write into Flash memory. So we need to use some external memory.

We will use the 24Cxx EEPROM chip, where xx can be 02, 04, 08, 16. The number represents the volume of the memory in kbits. As long as we need just 3 bytes to store, any of them will suit us. The price of the smallest 24C02 chip starts from $0.06 at AliExpress, so this is not a big price for such a convenience.

This EEPROM chip can be in a DIP-8, SOIC-8 or DFN-8 package, so it’s up to you what to choose. If you want to make the radio on a PCB, you might want to select the smallest option.

The pinout of the 24Cxx chip is shown in figure 9.

Figure 9. 24Cxx pinout.
Figure 9. 24Cxx pinout.

The power is applied to VCC and VSS, the supply voltage can be 2.7 to 5.5 V, so we will fit into this range if we select 3.3V to power the whole device. A0, A1, A2 are address pins. They allow us to set the I2C address of the EEPROM chip. We will connect them to ground to make the code compatible with all 24Cxx chips. The thing is that 24C02 has all A0, A1, A2 pins working, 24C04 has only A0 and A1, while A2 is internally tied to ground, 24C08 has only A0, and 24C16 doesn’t have any selectable address pins.

SCL and SDA are I2C bus pins.

WP is the write protect pin. It should be connected to ground for normal operation. If it’s connected to VCC, then writing to the memory is prohibited.

Writing to this chip is quite simple. You need to send the ship address first (0xA0), then the memory address to which you want to write, and then the data you want to write. After each data byte the memory address will be incremented, so sequential read is supported. The reading is implemented in the same way, but according to fig. 6. you first send the memory address from which you want to read the data, then you issue the Repeated start condition, and send the chip address with R/W = 1 (0xA1), after which you can read the data sequentially as well. If you need more information, feel free to use the datasheet.

Well, now we have all required information, and can consider the schematic diagram of the FM radio (figure 10).

Figure 10. Schematic diagram of the FM radio.
Figure 10. Schematic diagram of the FM radio.

As you can see, it’s quite straightforward. We have the microcontroller DD1, EEPROM chip DD2, and FM module DD3. GP1 pin of the microcontroller acts as SCL, and GP2 pin acts as SDA. R1 and R2 are I2C bus pull-up resistors. J1 is the 3.5 mm jack socket to connect the earphones. Ant is FM antenna or just a 75 cm piece of wire. Buttons SW1 and SW2 are multifunctional. When you short press them, the channel seek up or down will start. When you hold them longer than 200 ms, they change the volume - SW1 increases it, and SW2 decreases. As I mentioned before, supply voltage VCC should be no more than 3.3V.

Let’s now consider the code that implements the FM radio. It is quite long, as I warned...

#include "p10f200.inc"

i   EQU    10    ;Delay variable

j   EQU    11    ;Delay variable

bit_count   EQU    12    ;Counter of processed bits in I2C

i2c_data   EQU    13    ;Data to receive/transmit via I2C

port   EQU  14    ;Helper register to implement I2C

ack   EQU  15    ;Acknowledgment received from the device

volume   EQU    16    ;Radio volume level

frequency_l   EQU    17    ;Frequency low byte

frequency_h   EQU    18    ;Frequency high byte

count   EQU    19    ;Stores the time the button is pressed

button   EQU    1A    ;The number of button that is pressed

startup   EQU    1B    ;Indicates if it's the startup state

timer   EQU    1C    ;Counts time before storing the station

need_save   EQU    1D    ;Indicates if current station need to be saved

sda   EQU    GP2    ;SDA pin of the I2C

scl   EQU    GP1    ;SCL pin of the I2C

but_up   EQU    GP3    ;Button Volume up/Next station

but_down   EQU    GP0    ;Button Volume down/Previous station    

__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF

ORG 0x0000

INIT

MOVLW ~((1<<T0CS)|(1<<NOT_GPPU))

OPTION  ;Enable GPIO2 and pull-ups

MOVLW 0x0F   ;Save 0x0F into 'port' register

MOVWF port   ;It's used to switch SDA/SCL pins direction

TRIS GPIO   ;Set all pins as inputs

    

    MOVLW 0xFF   ;Perform 200 ms delay

    CALL DELAY   ;to let the power stabilize

    CLRF GPIO    ;Clear GPIO to set all pins to 0

    CLRF need_save   ;No need to save the station for now

    BSF startup, 0   ;Set 'startup' to 1 to indicate startup state

READ_EEPROM   ;Reading the stored data from EEPROM

    CALL I2C_START   ;Issue I2C start condition

    MOVLW 0xA0   ;EEPROM chip address for writing is 0xA0

    CALL I2C_WRITE_BYTE    ;Write the EEPROM address via I2C

    MOVLW 0x00   ;Set the EEPROM memory address to be read

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    CALL I2C_START   ;Issue I2C repeated start condition

    MOVLW 0xA1   ;EEPROM chip address for reading is 0xA1

    CALL I2C_WRITE_BYTE    ;Write EEPROM address via I2C

    CALL I2C_READ_BYTE    ;Read the EEPROM value at address 0x00

    CALL I2C_ACK   ;Issue acknowledgement

    MOVF i2c_data, W    ;Copy the 'i2c_data' into W register

    MOVWF volume   ;And store it into 'volume' register

    CALL I2C_READ_BYTE    ;Read the next EEPROM address

    CALL I2C_ACK

    MOVF i2c_data, W

    MOVWF frequency_l    ;and store its content into 'frequency_l'

    CALL I2C_READ_BYTE    ;Read the next EEPROM address

    CALL I2C_NACK   ;Issue Not acknowledgement, it's the last byte

    MOVF i2c_data, W

    MOVWF frequency_h    ;and store its content into 'frequency_h'

    CALL I2C_STOP   ;Issue Stop condition

    MOVLW 0xC0   ;Implement AND operation between 0xC0

    ANDWF frequency_l, F;and 'frequency_l' to clear its last 6 bits

    BSF frequency_l, 4    ;Set bit 4 (Tune) to adjust the frequency

START_RADIO   ;Start FM radio

    CALL I2C_START   ;Issue I2C Start condition

    MOVLW 0x20   ;Radio chip address for sequential writing is 0x20

    CALL I2C_WRITE_BYTE    ;Write the radio address via i2C

    MOVLW 0xC0   ;Write high byte into radio register 0x02

    CALL I2C_WRITE_BYTE    

    MOVLW 0x01   ;Write low byte into radio register 0x02

    CALL I2C_WRITE_BYTE

    MOVF frequency_h, W    ;Write high byte into radio register 0x03

    CALL I2C_WRITE_BYTE

    MOVF frequency_l, W    ;Write low byte into radio register 0x03

    CALL I2C_WRITE_BYTE

    CALL I2C_STOP   ;Issue I2C Stop condition

    MOVLW 0x0F   ;Implement AND operation between 0xC0

    ANDWF volume, F   ;and 'volume' to clear its higher 4 bits

    BSF volume, 7   ;Set bit 7  to select correct LNA input

    GOTO SET_VOLUME   ;And go to the 'SET_VOLUME' label

LOOP   ;Main loop of the program

    ;Beginning of the button 1 checking

    CALL CHECK_BUTTONS  ;Read the buttons state

    ANDLW 3   ;Clear all the bits of the result except two LSBs

    BTFSC STATUS, Z   ;If result is 0 (none of buttons were pressed)

    GOTO WAIT_FOR_TIMER;Then go to the 'WAIT_FOR_TIMER' label 

    MOVLW D'40'  ;Otherwise load initial value for the delay  

    CALL DELAY    ;and perform the debounce delay

    CALL CHECK_BUTTONS    ;Then check the buttons state again

    ANDLW 3    

    BTFSC STATUS, Z   ;If result is 0 (none of buttons were pressed)

    GOTO WAIT_FOR_TIMER;Then go to the 'WAIT_FOR_TIMER' label

    MOVWF button   ;Save the W value into the 'button'

    CLRF count   ;clear loop counter

BUTTONS_LOOP   ;Loop while button is pressed

    MOVLW 0xFF  ;Load initial value for the delay 200ms

    CALL DELAY    ;And perform the delay

    CALL CHECK_BUTTONS    ;Then check the buttons state again

    ANDLW 3

    BTFSC STATUS, Z   ;If state is 0 (it was a short press)

    GOTO CHANNEL_SEEK    ;Go to the 'CHANNEL_SEEK' label

    INCF count, F   ;Otherwise (long press) increment the counter

    BTFSS button, 0   ;Check the last bit of the 'button' register

    GOTO DECREASE_VOLUME;If it's 0 (Down), go to 'DECREASE_VOLUME'

INCREASE_VOLUME   ;Otherwise start 'INCREASE_VOLUME'

    INCF volume, F   ;Increment the 'volume' register

    BTFSC volume, 4   ;If bit 4 becomes set (volume = 0b10010000)

    DECF volume, F   ;then decrement the 'volume' to get 0b10001111

    GOTO SET_VOLUME   ;and go to the 'SET_VOLUME' label

DECREASE_VOLUME   ;Decrease the volume here

    DECF volume, F   ;Decrement the 'volume' register

    BTFSS volume, 7   ;If bit 7 becomes 0 (volume = 0b01111111)

    INCF volume, F   ;then increment the 'volume' to get 0b10000000

SET_VOLUME   ;Set the radio volume

    CALL I2C_START   ;Issue I2C start condition

    MOVLW 0x22   ;Radio chip address for random writing is 0x22

    CALL I2C_WRITE_BYTE    ;Write the radio address via I2C

    MOVLW 0x05   ;Set the register number to write to (0x05)

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    MOVLW 0x88   ;Set the high byte of 0x05 register (default value)

    CALL I2C_WRITE_BYTE    ;And write it via i2C

    MOVF volume, W   ;Set the 'volume' as low byte of 0x05 register

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    CALL I2C_STOP   ;Issue Stop condition

    BTFSS startup, 0    ;If 'startup' is 0 (not startup condition)

    GOTO BUTTONS_LOOP ;Then return to the 'BUTTONS_LOOP' label

    BCF startup, 0   ;Otherwise reset the 'startup' register

    GOTO LOOP   ;And return to the 'LOOP' label

CHANNEL_SEEK   ;Here button is released and we check what to do

    MOVF count, F   ;Check if 'count' register is 0

    BTFSS STATUS, Z   ;If 'count' is not 0 (it was a long press)

    GOTO SAVE_VOLUME    ;then go to the 'SAVE VOLUME' label

    CLRF timer   ;Otherwise (short press) we clear the 'timer'

    BSF need_save, 0    ;And set the 'need_save' register

    CALL I2C_START   ;Issue I2C Start condition

    MOVLW 0x20   ;Radio chip address for sequential writing is 0x20

    CALL I2C_WRITE_BYTE    ;Write the radio address via I2C

    BTFSS button, 0   ;Check the last bit of the 'button' register

    GOTO SEEK_DOWN   ;If it's 0 (button Down), go to 'SEEK_DOWN' label

    MOVLW 0xC3   ;Otherwise set 0xC3 as high byte of 0x02 register

    CALL I2C_WRITE_BYTE ;And write it via I2C

    MOVLW 0x01   ;Set 0x01 as low byte of 0x02 register

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    CALL I2C_STOP   ;Issue I2C Stop condition

    GOTO WAIT_FOR_TIMER;And go to the 'WAIT_FOR_TIMER' label

SEEK_DOWN   ;Seek the station down

    MOVLW 0xC1   ;Set 0xC1 as high byte of 0x02 register

    CALL I2C_WRITE_BYTE ;Ending of previous transaction

    MOVLW 0x01   ;Set 0x01 as low byte of 0x02 register

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    CALL I2C_STOP   ;Issue I2C Stop condition

    GOTO WAIT_FOR_TIMER;And go to the 'WAIT_FOR_TIMER' label

SAVE_VOLUME   ;Save the volume to the EEPROM

    CALL I2C_START   ;Issue I2C start condition

    MOVLW 0xA0   ;Set the EEPROM chip address to write

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    MOVLW 0x00   ;Set the EEPROM register address to write as 0x00

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    MOVF volume, W   ;Set the 'volume' as value to write to EEPROM

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    CALL I2C_STOP   ;Issue I2C stop condition

    GOTO LOOP   ;And return to the 'LOOP' labe;

WAIT_FOR_TIMER    ;Wait for 10 second to save the channel to EEPROM

    MOVLW D'45'   ;Set the delay about 40 ms

    CALL DELAY   ;And call the DELAY subroutine

    INCFSZ timer, F   ;Increase the 'timer' and check while it becomes 0

    GOTO LOOP   ;If it's not 0 then return to the 'LOOP' label

    

    BTFSS need_save, 0    ;Otherwise check the 'need_save' register

    GOTO LOOP   ;If it's 0 then return to the 'LOOP' register

    BCF need_save, 0    ;Otherwise clear the 'need_save' register

    CALL I2C_START   ;Issue I2C start condition

    MOVLW 0x22   ;Set the radic chip address for random writing

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    MOVLW 0x03   ;Set the radio register to read from (0x03)

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    CALL I2C_START   ;Issue I2C Repeated start condition

    MOVLW 0x23   ;Set the radio chip address for random reading

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    CALL I2C_READ_BYTE    ;Read the high byte of the register 0x03

    CALL I2C_ACK   ;Issue the Acknowledgement

    MOVF i2c_data, W    ;Copy the 'i2c_data' content into W register

    MOVWF frequency_h    ;And save it to the 'frequency_h' register

    CALL I2C_READ_BYTE    ;Read the low byte of the register 0x03

    CALL I2C_NACK   ;Issue the Not acknowledgement

    MOVF i2c_data, W    ;Copy the 'i2c_data' content into W register

    MOVWF frequency_l    ;And save it to the 'frequency_l' register

    CALL I2C_STOP   ;Issue I2C stop condition

    CALL I2C_START   ;Issue I2C start condition

    MOVLW 0xA0   ;Set the EEPROM chip address for writing

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    MOVLW 0x01   ;Set the EEPROM memory address for writing as 0x01

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    MOVF frequency_l, W    ;Load the 'frequency_l' content

    CALL I2C_WRITE_BYTE    ;And write it via I2C to the address 0x01

    BCF frequency_h, 7    ;Some weird thing, this bit is set for some reason

    MOVF frequency_h, W    ;Load the 'frequency_2' content

    CALL I2C_WRITE_BYTE    ;And write it via I2C to the address 0x02

    CALL I2C_STOP   ;Issue I2C Stop condition

    GOTO LOOP       ;loop forever

;-------------Check buttons---------------

CHECK_BUTTONS

    BTFSS GPIO, but_up    ;Check if button Up is pressed

    RETLW 1   ;and return 1 (b'01')

    BTFSS GPIO, but_down;Check if button Down is pressed

    RETLW 2   ;and return 2 (b'10')

    RETLW 0   ;If none of buttons is pressed then return 0

;-------------Helper subroutines---------------

SDA_HIGH   ;Set SDA pin high

    BSF port, sda   ;Set 'sda' bit in the 'port' to make it input

    MOVF port, W   ;Copy 'port' into W register

    TRIS GPIO   ;And set it as TRISGPIO value

    RETLW 0    

SDA_LOW   ;Set SDA pin low

    BCF port, sda   ;Reset 'sda' bit in the 'port' to make it output

    MOVF port, W   ;Copy 'port' into W register

    TRIS GPIO   ;And set it as TRISGPIO value

    RETLW 0

SCL_HIGH   ;Set SCL pin high

    BSF port, scl   ;Set 'scl' bit in the 'port' to make it input

    MOVF port, W   ;Copy 'port' into W register

    TRIS GPIO   ;And set it as TRISGPIO value

    RETLW 0

SCL_LOW   ;Set SCL pin low

    BCF port, scl   ;Reset 'scl' bit in the 'port' to make it output

    MOVF port, W   ;Copy 'port' into W register

    TRIS GPIO   ;And set it as TRISGPIO value

    RETLW 0

;-------------I2C start condition--------------

I2C_START    

    CALL SCL_HIGH   ;Set SCL high

    CALL SDA_LOW   ;Then set SDA low

    RETLW 0

;-------------I2C stop condition---------------

I2C_STOP    

    CALL SDA_LOW   ;Set SDA low

    CALL SCL_HIGH   ;Set SCL high

    CALL SDA_HIGH   ;Then set SDA highs and release the bus

    RETLW 0

;------------I2C write byte--------------------

I2C_WRITE_BYTE

    MOVWF i2c_data   ;Load 'i2c_data' from W register

    MOVLW 8   ;Load value 8 into 'bit_count'

    MOVWF bit_count    ;to indicate we're going to send 8 bits

I2C_WRITE_BIT   ;Write single bit to I2C

    CALL SCL_LOW   ;Set SCL low, now we can change SDA

    BTFSS i2c_data, 7    ;Check the MSB of 'i2c_data'

    GOTO I2C_WRITE_0    ;If it's 0 then go to the 'I2C_WRITE_0' label

I2C_WRITE_1   ;Else continue with 'I2C_WRITE_1'

    CALL SDA_HIGH   ;Set SDA high

    GOTO I2C_SHIFT   ;And go to the 'I2C_SHIFT' label

I2C_WRITE_0    

    CALL SDA_LOW   ;Set SDA low

I2C_SHIFT

    CALL SCL_HIGH   ;Set SCL high to start the new pulse

    RLF i2c_data, F   ;Shift 'i2c_data' one bit to the left

    DECFSZ bit_count, F;Decrement the 'bit_count' value, check if it's 0

    GOTO I2C_WRITE_BIT    ;If not then return to the 'I2C_WRITE_BIT'

I2C_CHECK_ACK   ;Else check the acknowledgement bit

    CALL SCL_LOW   ;Set I2C low to end the last pulse

    CALL SDA_HIGH   ;Set SDA high to release the bus

    CALL SCL_HIGH   ;Set I2C high to start the new pulse

    MOVF GPIO, W   ;Copy the GPIO register value into the 'ack'

    MOVWF ack   ;Now bit 'sda' of the 'ack' will contain ACK bit

    CALL SCL_LOW   ;Set SCL low to end the acknowledgement bit

    RETLW 0

;------------I2C read byte--------------------

I2C_READ_BYTE

    MOVLW 8   ;Load value 8 into 'bit_count'

    MOVWF bit_count    ;to indicate we're going to receive 8 bits

    CLRF i2c_data   ;Clear the 'i2c_data' register

I2C_READ_BIT   ;Read single bit from the I2C

    RLF i2c_data, F   ;Shift the 'i2c_data' register one bit to the left

    CALL SCL_LOW   ;Set SCL low to prepare for the new bit

    CALL SCL_HIGH   ;Set SCL high to read the bit value

    BTFSC GPIO, sda   ;Check the 'sda' bit in the GPIO register

    BSF i2c_data, 0   ;if it's 1 then set the LSB of the 'i2c_data'

    DECFSZ bit_count, F;Decrement the 'bit_count' value, check if it's 0

    GOTO I2C_READ_BIT    ;If not, then return to the 'I2C_READ_BIT'

    CALL SCL_LOW   ;Set SCL low to end the last pulse

    RETLW 0   ;Otherwise return from the subroutine

;----------I2C send ACK----------------------

I2C_ACK

    CALL SDA_LOW   ;Set SDA low to issue ACK condition

    CALL SCL_HIGH   ;Set SCL high to start the new pulse

    CALL SCL_LOW   ;Set SCL low to end the pulse

    CALL SDA_HIGH   ;Set SDA high to release the bus

    RETLW 0

;----------I2C send NACK----------------------

I2C_NACK

    CALL SDA_HIGH   ;Set SDA low to issue NACK condition

    CALL SCL_HIGH   ;Set SCL high to start the new pulse

    CALL SCL_LOW   ;Set SCL low to end the pulse

    RETLW 0

;-------------Delay subroutine--------------

DELAY    ;Start DELAY subroutine here  

MOVWF i    ;Copy the value to the register i

MOVWF j    ;Copy the value to the register j

DELAY_LOOP    ;Start delay loop

DECFSZ i, F    ;Decrement i and check if it is not zero

GOTO DELAY_LOOP  ;If not, then go to the DELAY_LOOP label

DECFSZ j, F    ;Decrement j and check if it is not zero

GOTO DELAY_LOOP  ;If not, then go to the DELAY_LOOP label

RETLW 0    ;Else return from the subroutine

    END

This is the longest program we have ever written. And, I think, the most complicated.

Let’s first consider the names of the registers, as we use almost all of our available RAM: 14 bytes of the 16.

  • Registers ‘i’ and ‘j’ as usual are used for the delay implementation.
  • ‘bit_count’ register is used as a counter of bits in the data byte to transmit/receive via I2C.
  • ‘i2c_data’ register represents the data to be transmitted or the data that was received via I2C.
  • ‘port’ is a very important register for the I2C implementation. I’ll describe it more in detail when I tell about I2C realization.
  • ‘ack’ consists of the ACK/NACK reply from the device in the transmitting mode.
  • ‘volume’ represents the sound volume of the FM radio.
  • ;frequency_h’ and ‘frequency_l’ in fact represent the higher and lower bytes of the register 0x03 of the FM radio chip. But as follows from their name, they are used to save the frequency of the current station.
  • ‘count’ register is used to count the number of 200 ms intervals during which buttons are held.
  • ‘button’ register consists of the number of buttons that are pressed: 0 - no button is pressed, 1 - button Up is pressed, 2 - button Down is pressed.
  • ‘startup’ register is used to indicate that we are in startup state, it’s required as we need to implement certain steps only once after power-up.
  • ‘timer’ register is not actually related to the Timer0 of the microcontroller. It counts the time since the last button press to save the station frequency in the EEPROM.
  • ‘need_save’ indicates if the frequency is needed to be saved in the EEPROM.

Also, this time we define the special names to each GPIO pin (lines 17-20).

sda   EQU    GP2    ;SDA pin of the I2C

scl   EQU    GP1    ;SCL pin of the I2C

but_up   EQU    GP3    ;Button Volume up/Next station

but_down   EQU    GP0    ;Button Volume down/Previous station

Let’s now consider the initialization part (lines 26-86).

INIT

MOVLW ~((1<<T0CS)|(1<<NOT_GPPU))

OPTION  ;Enable GPIO2 and pull-ups

MOVLW 0x0F   ;Save 0x0F into 'port' register

MOVWF port   ;It's used to switch SDA/SCL pins direction

TRIS GPIO   ;Set all pins as inputs

In the beginning we configure GP2 as GPIO and enable the pull-up resistors on each GPIO pin (lines 27, 28)

Then we load the value 0xFF into the ‘port’ register (line 30) and the TRISGPIO register (line 31) to configure all the GPIOs as inputs. As for the ‘port’ register, have more patience, I will explain it later.

MOVLW 0xFF   ;Perform 200 ms delay

CALL DELAY   ;to let the power stabilize

    CLRF GPIO    ;Clear GPIO to set all pins to 0

    CLRF need_save   ;No need to save the station for now

    BSF startup, 0   ;Set 'startup' to 1 to indicate startup state

In lines 33-34 we perform a 200 ms delay to let the power stabilize. When I didn’t use it, I had some issues sometimes: the frequency or the volume might be set incorrectly.

In lines 36-38 we initialize the registers: clear GPIO register to set all pins low (line 36), clear ‘need_save’ register (line 37), as we don’t need to save the frequency to EEPROM at startup, and set bit 0 of the ‘startup’ register to indicate that we need to implement the power-up routine.

In lines 40-61 we implement reading the EEPROM memory. I’ll describe this part in detail, and then will skip the detailed description of other I2C communication parts, as they are the same, we just set different addresses and receive/transmit different data.

READ_EEPROM   ;Reading the stored data from EEPROM

CALL I2C_START   ;Issue I2C start condition

MOVLW 0xA0   ;EEPROM chip address for writing is 0xA0

    CALL I2C_WRITE_BYTE ;Write the EEPROM address via I2C

    MOVLW 0x00   ;Set the EEPROM memory address to be read

    CALL I2C_WRITE_BYTE ;And write it via I2C

In line 41 we call the ‘I2C_START’ subroutine (I’ll describe the I2C protocol in the end after the description of the main logic of the program). As I mentioned before, every I2C communication, whether it is transmitting or receiving, should begin with the Start condition. If you begin with just sending the data byte, the devices will not understand you.

After that, we need to transmit the address of the device with which we want to communicate. As I mentioned before, the EEPROM chip address is 0xA0, thus we load this value into the W register (line 42) and call the ‘I2C_WRITE_BYTE’ subroutine (line 43). If the device is present, it will reply with ACK. Frankly, I didn’t process the acknowledgement bit because of the lack of the memory, so even if the device is not present, “the show must go on”, and the Master will keep sending the data.

To read data from the EEPROM we need to set the memory address from which we want to read. I started from the very beginning - from 0x00. At this address we store the ‘volume’, the next two addresses hold ‘frequency_l’ and ‘frequency_h’ correspondingly.

So we load the value 0x00 into the W register (line 44) and write it via I2C (line 45).

CALL I2C_START   ;Issue I2C repeated start condition

MOVLW 0xA1   ;EEPROM chip address for reading is 0xA1

    CALL I2C_WRITE_BYTE ;Write EEPROM address via I2C

    CALL I2C_READ_BYTE   ;Read the EEPROM value at address 0x00

    CALL I2C_ACK   ;Issue acknowledgement

    MOVF i2c_data, W    ;Copy the 'i2c_data' into W register

    MOVWF volume   ;And store it into 'volume' register

After that, we want to switch from sending data to receiving data, thus we issue the Repeated start condition (line 46) and then send the EEPROM chip address with the R/W bit set to 1, and thus the address now is 0xA1 (lines 47, 48). Now we are ready to receive the data from the EEPROM.

In line 49, we call the ‘I2C_READ_BYTE’ subroutine, after which we will have the content of the EEPROM memory address 0x00 in the ‘i2c_data’ register. As we want to read more bytes, we send the acknowledgement bit by calling ‘I2C_ACK’ (line 50).

Then we need to copy the ‘i2c_data’ content into the proper register. In this case, it is the ‘volume’, and we do this in lines 51 and 52.

CALL I2C_READ_BYTE  ;Read the next EEPROM address

CALL I2C_ACK

    MOVF i2c_data, W

    MOVWF frequency_l    ;and store its content into 'frequency_l'

    CALL I2C_READ_BYTE  ;Read the next EEPROM address

    CALL I2C_NACK   ;Issue Not acknowledgement, it's the last byte

    MOVF i2c_data, W

    MOVWF frequency_h    ;and store its content into 'frequency_h'

    CALL I2C_STOP   ;Issue Stop condition

Then we read the next EEPROM address and save it into the ‘frequency_l’ register (lines 53-56). The procedure is the same as with the ‘volume’ so no need for detailed explanation.

The last we read the ‘frequency_h’ value (lines 57-60). To indicate that we’re not going to read more data, we send the NACK bit instead of ACK by calling the ‘I2C_NACK’ subroutine (line 58).

The last thing to complete the I2C communication is to issue the Stop condition by calling the ‘I2C_STOP’ subroutine.

As you see, there is nothing too complex: you just need to strictly follow the protocol rules, and everything will be fine.

MOVLW 0xC0   ;Implement AND operation between 0xC0

    ANDWF frequency_l, F ;and 'frequency_l' to clear its last 6 bits

    BSF frequency_l, 4   ;Set bit 4 (Tune) to adjust the frequency

As I mentioned before, the ‘frequency_h’ and ‘frequency_l’ registers represent the higher and lower bytes of the FM chip register 0x03. If you look at its description, you’ll see that bits 15 to 6 stand for the ‘CHAN’, bit 4 stands for ‘TUNE’, and bits 5, 3-0 should be set to 0.

Initially, when the EEPROM is blank, all its memory cells are 0xFF, so if we read the EEPROM the first time, we can get the unpredictable result by having bits 5, 3-0 set to 1. To avoid this situation we format the ‘frequency_l’ register (which consists bits 7-0 of FM chip register 0x03) by ANDing it with the value 0xC0 (which is 0b11000000), after that bits 5 to 0 will be cleared (lines 63, 64). Then we need to set the bit ‘TUNE’ (bit 5) to 1, and we do this in line 65. After that we’re good to write the data into the FM chip, and we proceed to the ‘START_RADIO’ part (line 67).

START_RADIO   ;Start FM radio

CALL I2C_START   ;Issue I2C Start condition

    MOVLW 0x20   ;Radio chip address for sequential writing is 0x20

    CALL I2C_WRITE_BYTE ;Write the radio address via i2C

    MOVLW 0xC0   ;Write high byte into radio register 0x02

    CALL I2C_WRITE_BYTE    

    MOVLW 0x01   ;Write low byte into radio register 0x02

    CALL I2C_WRITE_BYTE

    MOVF frequency_h, W ;Write high byte into radio register 0x03

    CALL I2C_WRITE_BYTE

    MOVF frequency_l, W ;Write low byte into radio register 0x03

    CALL I2C_WRITE_BYTE

    CALL I2C_STOP   ;Issue I2C Stop condition

We don’t forget about the Start condition (line 68) and then we send the FM chip address for the sequential writing, which is 0x20 (lines 69-70). As I mentioned before, in this mode the next transmitted byte will be written as the upper byte of the 0x02 register, the next one will be represented as the lower byte of the 0x02 register, then the same for the 0x03 register and so on and so far. So we load the value 0xC001 to the register 0x02 (lines 71-74). If you look at its description, you may see that we set the bits DHIZ, DMUTE, and ENABLE, to enable audio outputs, unmute the radio, and enable it. After implementing these lines, the FM radio will be ready to operate, and you will hear the click when it turns on.

Then we consequently load the values of the ‘frequency_h’ and ‘frequency_l’ registers into the FM chip register 0x03 (lines 75-78). After that, the radio will be loaded with the preset station frequency, and start to translate it. When you first run your radio, the ‘CHAN’ value will be 0b1111111111 which most likely is not a correct value, so you will just hear noise. But don’t worry, we still have the buttons to choose our favorite station and save it.

To end the I2C communication we issue the Stop condition (line 79).

MOVLW 0x0F   ;Implement AND operation between 0xC0

    ANDWF volume, F   ;and 'volume' to clear its higher 4 bits

    BSF volume, 7   ;Set bit 7  to select correct LNA input

    GOTO SET_VOLUME   ;And go to the 'SET_VOLUME' label

Now we need to format the ‘volume’ register, as it represents the lower byte of the register 0x05. If you look at its description, you can see that the upper 12 bits are 0b100010001000, and the last four bits represent the volume. So in the ‘volume’ register we need to clear the upper four bits, and then set bit 7, which we do in lines 81-83. So when you run your radio the first time, the volume will be set as 0b1111 which is the maximum value, so you will hear the click, and then quite a loud noise. And again, don’t worry, we’ll fix that.

After we prepare the ‘volume’ register, we go to the ‘SET_VOLUME’ label (line 84) which is located at line 119. I’ll postpone its description until we get to it.

Now the initialization is finished, and we start the main infinite loop of the program (lines 86 - 209).

;-------------Check buttons---------------

CHECK_BUTTONS

BTFSS GPIO, but_up   ;Check if button Up is pressed

    RETLW 1   ;and return 1 (b'01')

    BTFSS GPIO, but_down ;Check if button Down is pressed

    RETLW 2   ;and return 2 (b'10')

    RETLW 0   ;If none of buttons is pressed then return 0

First thing that we need to do is to check the state of the buttons. We do it by calling the ‘CHECK_BUTTONS’ subroutine (line 88). This subroutine is located at lines 212-217, and is the same as in Tutorial 10 devoted to the code lock, and I will not describe it in detail.

LOOP      ;Main loop of the program

;Beginning of the button 1 checking

    CALL CHECK_BUTTONS   ;Read the buttons state

    ANDLW 3   ;Clear all the bits of the result except two LSBs

    BTFSC STATUS, Z   ;If result is 0 (none of buttons were pressed)

    GOTO WAIT_FOR_TIMER  ;Then go to the 'WAIT_FOR_TIMER' label 

    MOVLW D'40'  ;Otherwise load initial value for the delay  

    CALL DELAY    ;and perform the debounce delay

    CALL CHECK_BUTTONS  ;Then check the buttons state again

    ANDLW 3    

    BTFSC STATUS, Z   ;If result is 0 (none of buttons were pressed)

    GOTO WAIT_FOR_TIMER  ;Then go to the 'WAIT_FOR_TIMER' label

    MOVWF button      ;Save the W value into the 'button'

    CLRF count   ;clear loop counter

So, in lines 88-90 we check if any of the buttons are pressed. If not, we go to the ‘WAIT_FOR_TIMER’ label (line 91). If one of the buttons is pressed, we perform the debounce delay (lines 92-93), and check the button state again (lines 94-97). If it’s not pressed, then there was some noise, and we go to the ‘WAIT_FOR_TIMER’ subroutine (line 94). If button is still pressed, we finally start its processing.

As I mentioned earlier, the buttons are multifunctional: if we press them for a short press, then we seek the next/previous station, and if we press them for a long hold, then we change the volume.

First, we need to save the number of the button that was pressed into the ‘button’ register (line 98) as when we release the button, its number will be lost and read as 0. Then we clear the ‘count’ value (line 99) and start the ‘BUTTON_LOOP’ part (lines 100-133).

BUTTONS_LOOP      ;Loop while button is pressed

MOVLW 0xFF  ;Load initial value for the delay 200ms

    CALL DELAY    ;And perform the delay

    CALL CHECK_BUTTONS  ;Then check the buttons state again

    ANDLW 3

    BTFSC STATUS, Z   ;If state is 0 (it was a short press)

    GOTO CHANNEL_SEEK    ;Go to the 'CHANNEL_SEEK' label

    INCF count, F   ;Otherwise (long press) increment the counter

    BTFSS button, 0   ;Check the last bit of the 'button' register

    GOTO DECREASE_VOLUME ;If it's 0 (Down), go to 'DECREASE_VOLUME'

In lines 101-102 we perform a 200 ms delay and check if a button is still pressed (lines 103-105). If a button is not pressed anymore, that means that it was a short press, and we need to go to the ‘CHANNEL_SEEK’ label (line 106). Otherwise we consider the press as a long one, and stay in the ‘BUTTON_LOOP’. We increment the ‘count’ value (line 107) to let the program know after releasing the button that it was a long press, and we don’t need to change the frequency.

Then we check the LSB of the ‘button’ register (line 108). If it’s ‘1’ then the button Up was pressed and line 109 is skipped, and we go to line 110 and start the ‘INCREASE_VOLUME’ part (lines 110-114). Otherwise (if LSB of the ‘button’ register is 0, which means that ‘button’ = 2 or 0b10) line 109 is implemented and we get to the ‘DECREASE_VOLUME’ part (lines 115-118).

INCREASE_VOLUME   ;Otherwise start 'INCREASE_VOLUME'

    INCF volume, F   ;Increment the 'volume' register

    BTFSC volume, 4   ;If bit 4 becomes set (volume = 0b10010000)

    DECF volume, F   ;then decrement the 'volume' to get 0b10001111

    GOTO SET_VOLUME   ;and go to the 'SET_VOLUME' label

DECREASE_VOLUME   ;Decrease the volume here

    DECF volume, F   ;Decrement the 'volume' register

    BTFSS volume, 7   ;If bit 7 becomes 0 (volume = 0b01111111)

    INCF volume, F   ;then increment the 'volume' to get 0b10000000

Let’s consider the ‘INCREASE_VOLUME’ part first. We increment the ‘volume’ register (line 111), and then check if bit 4 of the ‘volume’ register becomes 1 (line 112). If that happened, it means that the volume value overflowed and became 0b10010000. In this case we need to decrement the ‘volume’ value (line 113) in order to set it to 0b10001111 and keep it at maximum value. Once the volume is set, we go to the ‘SET_VOLUME’ part (line 114).

The ‘DECREASE_VOLUME’ does the opposite. We first decrement the ‘volume’ register (line 116) and check if bit 7 becomes 0 (line 117), which means that value is underflowed and became 0b01111111. In this case we need to increment the ‘volume’ value (line 118) in order to keep it at minimum value 0b10000000.

SET_VOLUME   ;Set the radio volume

CALL I2C_START   ;Issue I2C start condition

    MOVLW 0x22   ;Radio chip address for random writing is 0x22

    CALL I2C_WRITE_BYTE  ;Write the radio address via I2C

    MOVLW 0x05   ;Set the register number to write to (0x05)

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    MOVLW 0x88   ;Set the high byte of 0x05 register (default value)

    CALL I2C_WRITE_BYTE  ;And write it via i2C

    MOVF volume, W   ;Set the 'volume' as low byte of 0x05 register

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    CALL I2C_STOP   ;Issue Stop condition

    BTFSS startup, 0     ;If 'startup' is 0 (not startup condition)

    GOTO BUTTONS_LOOP ;Then return to the 'BUTTONS_LOOP' label

    BCF startup, 0   ;Otherwise reset the 'startup' register

    GOTO LOOP   ;And return to the 'LOOP' label

Now we are ready to consider the ‘SET_VOLUME’ part, as we have three links to it: from ‘INCREASE_VOLUME’, from ‘DECREASE_VOLUME’, and from ‘START_RADIO’, if you remember.

This time we will communicate with the FM chip using its address for random access, which is 0x22 (lines 121-122). As we remember, the VOLUME bits are located in register 0x05, so we need to send that (lines 123-124). Then we write 0x88 (0b10001000) as the upper byte of this register (lines 125-126) and ‘volume’ content as the lower byte (line 127-128). After we finish the I2C communication (line 129) we check the ‘startup’ register (line 130). If it’s 0 (not a startup mode) then we return to the ‘BUTTON_LOOP’ label (line 131). If ‘startup’ is 1 (which means that we got here from the ‘START_RADIO’ part) we skip line 131, and go to line 132 where we reset the ‘startup’ register, and then return to the ‘LOOP’ label (line 132), and thus we complete the power-up routine.

Once the button is released we get to the ‘CHANNEL_SEEK’ part (lines 134-157). First, we check the ‘count’ value with the MOVF instruction (line 135) (I hope you remember its implementation in this role. If we use it to copy the register into itself, this affects the Z bit of the STATUS register, and thus we can check if the register’s value is 0). If it’s not zero (line 136) that means that it was a long press, and we go to the ‘SAVE_VOLUME’ label (line 137) where we will save the volume into the EEPROM.

CHANNEL_SEEK      ;Here button is released and we check what to do

    MOVF count, F   ;Check if 'count' register is 0

    BTFSS STATUS, Z   ;If 'count' is not 0 (it was a long press)

    GOTO SAVE_VOLUME     ;then go to the 'SAVE VOLUME' label

    CLRF timer   ;Otherwise (short press) we clear the 'timer'

    BSF need_save, 0     ;And set the 'need_save' register

    CALL I2C_START   ;Issue I2C Start condition

    MOVLW 0x20   ;Radio chip address for sequential writing is 0x20

    CALL I2C_WRITE_BYTE  ;Write the radio address via I2C

    BTFSS button, 0   ;Check the last bit of the 'button' register

    GOTO SEEK_DOWN   ;If it's 0 (button Down), go to 'SEEK_DOWN' label

    MOVLW 0xC3   ;Otherwise set 0xC3 as high byte of 0x02 register

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    MOVLW 0x01   ;Set 0x01 as low byte of 0x02 register

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    CALL I2C_STOP   ;Issue I2C Stop condition

    GOTO WAIT_FOR_TIMER  ;And go to the 'WAIT_FOR_TIMER' label

Otherwise we start channel seeking. First, we reset the ‘timer’ register (line 138). When the timer value expires, the current station is saved into the EEPROM. I set this time to be about 10 seconds, which is enough to listen to the station and recognize if you like it. Then we set the ‘need_save’ register to 1 (line 139) to indicate that we expect to save the frequency into the EEPROM after the timeout.

Now we need to start the channel seeking procedure by setting the bit SEEK in register 0x02 of the FM chip. Also, we need to set the bit SEEKUP of the same register if button Up was pressed. As we will write to channel 0x02, we can use the FM chip address 0x20 for the sequential access, and thus save a couple of lines of the code.

So we initiate a new I2C communication (line 140), transmit the chip address 0x20 (lines 141-142), then we check the LSB of the ‘button’. If it’s 1 (button Up) then line 144 is skipped and we go to line 145, where we transmit 0xC3 as a higher byte of the 0x02 register (bits DHIZ, DMUTE, SEEKUP, and SEEK are set) (lines 145-146), then we transmit 0x01 as a lower byte (bit ENABLE is set) (lines 147-148) and end the communication (line 149). Then we go wait while 10 seconds expire to save the current station (line 150).

SEEK_DOWN   ;Seek the station down

MOVLW 0xC1   ;Set 0xC1 as high byte of 0x02 register

    CALL I2C_WRITE_BYTE  ;Ending of previous transaction

    MOVLW 0x01   ;Set 0x01 as low byte of 0x02 register

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    CALL I2C_STOP   ;Issue I2C Stop condition

    GOTO WAIT_FOR_TIMER  ;And go to the 'WAIT_FOR_TIMER' label

The ‘SEEK_DOWN’ block (lines 151-157) ], where we get from the line 144 if it’s not skipped, is the same as described above, the only difference is that we transmit 0xC1 (bit SEEKUP is cleared) as the higher byte of the 0x02 register (line 152).

SAVE_VOLUME   ;Save the volume to the EEPROM

CALL I2C_START   ;Issue I2C start condition

    MOVLW 0xA0   ;Set the EEPROM chip address to write

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    MOVLW 0x00        ;Set the EEPROM register address to write as 0x00

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    MOVF volume, W   ;Set the 'volume' as value to write to EEPROM

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    CALL I2C_STOP   ;Issue I2C stop condition

    GOTO LOOP   ;And return to the 'LOOP' labe;

The “SAVE_VOLUME’ block (lines 159-168) is used to save the ‘volume’ value into the EEPROM address 0x00. It doesn’t have anything new, so I’ll skip it’s explanation.

And now we finally reached the ‘WAIT_FOR_TIMER’ part (lines 170-207). First, we implement the delay of about 40 ms (lines 171,172). This value is not random. The timer overflows when it reaches the value 255, so 255 x 40 = 10 200 ms, or about 10 seconds.

WAIT_FOR_TIMER           ;Wait for 10 second to save the channel to EEPROM

MOVLW D'45'   ;Set the delay about 40 ms

    CALL DELAY   ;And call the DELAY subroutine

    INCFSZ timer, F   ;Increase the 'timer' and check while it becomes 0

    GOTO LOOP   ;If it's not 0 then return to the 'LOOP' label

    

    BTFSS need_save, 0  ;Otherwise check the 'need_save' register

    GOTO LOOP   ;If it's 0 then return to the 'LOOP' register

    BCF need_save, 0     ;Otherwise clear the 'need_save' register

In line 173 we meet a new instruction. First time in the last several tutorials! It’s INCFSZ (Increment F register, skip if it’s zero). Actually it’s almost the same as DECFSZ but it increments the register instead of decrementing it. So we increment the ‘timer’ value til it reaches 255. In the next iteration it becomes 0, and line 174 (where we returned to the ‘LOOP’ label) is skipped. And we finally can save the current frequency into the EEPROM, if needed.

CALL I2C_START   ;Issue I2C start condition

    MOVLW 0x22   ;Set the radic chip address for random writing

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    MOVLW 0x03   ;Set the radio register to read from (0x03)

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    CALL I2C_START   ;Issue I2C Repeated start condition

    MOVLW 0x23   ;Set the radio chip address for random reading

    CALL I2C_WRITE_BYTE  ;And write it via I2C

    CALL I2C_READ_BYTE  ;Read the high byte of the register 0x03

    CALL I2C_ACK      ;Issue the Acknowledgement

    MOVF i2c_data, W     ;Copy the 'i2c_data' content into W register

    MOVWF frequency_h    ;And save it to the 'frequency_h' register

    CALL I2C_READ_BYTE  ;Read the low byte of the register 0x03

    CALL I2C_NACK   ;Issue the Not acknowledgement

    MOVF i2c_data, W     ;Copy the 'i2c_data' content into W register

    MOVWF frequency_l    ;And save it to the 'frequency_l' register

    CALL I2C_STOP   ;Issue I2C stop condition

To check if we need to save the frequency, we check the ‘need_save’ register (line 176). If it’s 0 then we return to the ‘LOOP’ label without saving anything (line 177). Otherwise we clear the ‘need_save’ register (line 178) not to save the frequency the next time again and read the register 0x03 from the FM radio chip (lines 179-195). As we want to read the register 0x03, we have to use the FM chip address for random access - 0x22 (line 180). And after specifying the register number from which we want to read (lines 182-183) we don’t forget to issue the repeated start condition and send the FM chip address but with set bit R/W, so it becomes 0x23 (line 185). And also we don’t forget to send NACK after the last read byte (line 192).

CALL I2C_START     ;Issue I2C start condition

    MOVLW 0xA0     ;Set the EEPROM chip address for writing

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    MOVLW 0x01         ;Set the EEPROM memory address for writing as 0x01

    CALL I2C_WRITE_BYTE    ;And write it via I2C

    MOVF frequency_l, W    ;Load the 'frequency_l' content

    CALL I2C_WRITE_BYTE    ;And write it via I2C to the address 0x01

    BCF frequency_h, 7     ;Some weird thing, this bit is set for some reason

    MOVF frequency_h, W    ;Load the 'frequency_2' content

    CALL I2C_WRITE_BYTE    ;And write it via I2C to the address 0x02

    CALL I2C_STOP     ;Issue I2C Stop condition

    GOTO LOOP          ;loop forever

In lines 197-207 we save the ‘frequency_l’ and ‘frequency_h’ values into the EEPROM at memory addresses 0x01 and 0x02 correspondingly. Line 204, where we clear the MSB of the ‘frequency_h’ register makes no sense to me, but for some reason it always was set to 1, and thus the value was saved wrong, after adding this line, everything becomes fine.

So this is how the main logic of the program works. It’s not that difficult as it might seem initially, it just has a cumbersome realization. Now let’s consider how the implementation of the I2C bus is done.

First, I created some helper subroutines: SDA_HIGH (lines 219-223), SDA_LOW (lines 225-229), SCL_HIGH (lines 231-235), SCL_LOW (lines 237-241). The names of the subroutines speak for themselves. They are used to set the corresponding pin (SDA or SCL) into the corresponding state - high or low. But why couldn't we just use “BSF GPIO, SDA”, and “BCF GPIO, SDA”?

;-------------Helper subroutines---------------

SDA_HIGH   ;Set SDA pin high

BSF port, sda    ;Set 'sda' bit in the 'port' to make it input

    MOVF port, W   ;Copy 'port' into W register

    TRIS GPIO ;And set it as TRISGPIO value

    RETLW 0    

SDA_LOW    ;Set SDA pin low

    BCF port, sda    ;Reset 'sda' bit in the 'port' to make it output

    MOVF port, W   ;Copy 'port' into W register

    TRIS GPIO    ;And set it as TRISGPIO value

    RETLW 0

SCL_HIGH   ;Set SCL pin high

    BSF port, scl    ;Set 'scl' bit in the 'port' to make it input

    MOVF port, W   ;Copy 'port' into W register

    TRIS GPIO    ;And set it as TRISGPIO value

    RETLW 0

SCL_LOW    ;Set SCL pin low

    BCF port, scl    ;Reset 'scl' bit in the 'port' to make it output

    MOVF port, W   ;Copy 'port' into W register

    TRIS GPIO    ;And set it as TRISGPIO value

    RETLW 0

The thing is that we need to emulate the open-collector output. The GPIO pins of the PIC10F200 microcontroller in the output mode are push-pull, not open-collector. So this is not good for the I2C bus implementation. Thus, to set the line high we have to configure the pin as an input, and then the line will be pulled up by the resistor. To set the line low, we have to configure the line as an output and set the corresponding GPIO pin to 0 (we actually already have done this in line 36, where we cleared the GPIO register). So we need to reconfigure a pin to be either input or output, and here another problem arises. We can’t apply the read-modify-write instructions, like BSF or BCF to the TRISGPIO register directly, because we can’t read it, only write using the TRIS instruction. And to have an opportunity of using BCF and BSF instruction we need some auxiliary register which we can read and write. As you may have already guessed, this is the ‘port’ register.

The main idea is that we modify the ‘port’ register using the BCF or BSF instructions, and then copy its value into the TRISGPIO register using the TRIS instruction. Now let’s consider one of the helper subroutines more detailed. Let it be the ‘SDA_HIGH’.

We set the bit ‘sda’ of the ‘port’ register to 1 (line 220), then copy the value of the ‘port’ into the W register (line 221) and finally copy W register into the TRISGPIO register (line 222). By doing this we configure the ‘sda’ pin as input, and it is tied to the VCC by means of the pull-up resistor.

The other subroutines work the same way, we just set bit ‘sda’ or ‘scl’ high or low.

;-------------I2C start condition--------------

I2C_START    

CALL SCL_HIGH    ;Set SCL high

    CALL SDA_LOW   ;Then set SDA low

    RETLW 0

;-------------I2C stop condition---------------

I2C_STOP    

    CALL SDA_LOW   ;Set SDA low

    CALL SCL_HIGH    ;Set SCL high

    CALL SDA_HIGH    ;Then set SDA highs and release the bus

    RETLW 0

Let’s finally consider how the I2C interface is implemented in our program.

‘I2C_START’ subroutine (lines 244-247) issues two states - Start and Repeated Start. As you can see, to implement these states we first set the SCL line high (line 245), and then set SDA low (line 246) while SCL is high, see figure 3.

‘I2C_STOP’ subroutine (lines 249-253) issues the Stop condition. First, it sets SDA low (line 250) just in case it was high before. Second, it sets SCL high (line 251), and finally sets SDA high (line 252) while SCL is high, see figure 3.

;------------I2C write byte--------------------

I2C_WRITE_BYTE

MOVWF i2c_data     ;Load 'i2c_data' from W register

    MOVLW 8      ;Load value 8 into 'bit_count'

    MOVWF bit_count    ;to indicate we're going to send 8 bits

I2C_WRITE_BIT          ;Write single bit to I2C

    CALL SCL_LOW       ;Set SCL low, now we can change SDA

    BTFSS i2c_data, 7  ;Check the MSB of 'i2c_data'

    GOTO I2C_WRITE_0  ;If it's 0 then go to the 'I2C_WRITE_0' label

‘I2C_WRITE_BYTE’ subroutine (lines 255-280) transmits one byte via the I2C bus and checks the ACK bit. First, we need to copy the value from the W register into the ‘i2c_data’ register (line 256), as we will work with it. Then we set the ‘bit_count’ value as 8 (lines 257, 258) to indicate that we want to transmit 8 bits. Then we start the ‘I2C_WRITE_BIT’ block (lines 259-272). First, we set SCL low to start the new pulse (line 260). Now, as SCL is low, we are free to change the SDA line state. We check the MSB of the ‘i2c_data’ (line 261) as according to the I2C standard, the byte is transmitted/received with the MSB first.

I2C_WRITE_1   ;Else continue with 'I2C_WRITE_1'

CALL SDA_HIGH   ;Set SDA high

    GOTO I2C_SHIFT   ;And go to the 'I2C_SHIFT' label

I2C_WRITE_0    

    CALL SDA_LOW         ;Set SDA low

I2C_SHIFT

    CALL SCL_HIGH   ;Set SCL high to start the new pulse

    RLF i2c_data, F   ;Shift 'i2c_data' one bit to the left

    DECFSZ bit_count, F  ;Decrement the 'bit_count' value, check if it's 0

    GOTO I2C_WRITE_BIT  ;If not then return to the 'I2C_WRITE_BIT'

If this bit is 0 then line 262 is not skipped, and we go to the ‘I2C_WRITE_0’ label where we set the SDA line low (line 267). If the MSB is 1, then line 262 is skipped and we get to the ‘I2C_WRITE_1’ block where we set the SDA high (line 264) and go to the ‘I2C_SHIFT’ label (line 265). There we set the SCL line high (line 269) to indicate that the data on the SDA line is valid and may be read by the Slave device. Then we shift the ‘i2c_data’ register at one bit to the left (line 270), and check the ‘bit_count’ value (line 271). If it’s not 0 then we return to the ‘I2C_WRITE_BIT’ label (line 272), otherwise we consider that we have transmitted the whole byte, and now need to check the ACK bit in the ‘I2C_CHECK_ACK’ block (lines 273-279).

I2C_CHECK_ACK    ;Else check the acknowledgement bit

CALL SCL_LOW   ;Set I2C low to end the last pulse

    CALL SDA_HIGH    ;Set SDA high to release the bus

    CALL SCL_HIGH    ;Set I2C high to start the new pulse

    MOVF GPIO, W   ;Copy the GPIO register value into the 'ack'

    MOVWF ack    ;Now bit 'sda' of the 'ack' will contain ACK bit

    CALL SCL_LOW   ;Set SCL low to end the acknowledgement bit

    RETLW 0

We start the new pulse by setting SCL low (line 274), then we set SDA high (line 275) to release this line, so if the Slave device will pull it down, we will know that we receive the ACK bit from it, and not because we held this line low. Then we set SCL high (276). Now the Slave device should not change the SDA line, and it’s safe to read it, which we do in line 277, and then save the result into the ‘ack’ register (line 278). Finally, we set the SCL low (line 279) to end the acknowledgement bit.

;------------I2C read byte--------------------

I2C_READ_BYTE

MOVLW 8   ;Load value 8 into 'bit_count'

    MOVWF bit_count      ;to indicate we're going to receive 8 bits

    CLRF i2c_data   ;Clear the 'i2c_data' register

I2C_READ_BIT       ;Read single bit from the I2C

    RLF i2c_data, F   ;Shift the 'i2c_data' register one bit to the left

    CALL SCL_LOW       ;Set SCL low to prepare for the new bit

    CALL SCL_HIGH   ;Set SCL high to read the bit value

    BTFSC GPIO, sda   ;Check the 'sda' bit in the GPIO register

    BSF i2c_data, 0   ;if it's 1 then set the LSB of the 'i2c_data'

    DECFSZ bit_count, F  ;Decrement the 'bit_count' value, check if it's 0

    GOTO I2C_READ_BIT    ;If not, then return to the 'I2C_READ_BIT'

    CALL SCL_LOW       ;Set SCL low to end the last pulse

    RETLW 0   ;Otherwise return from the subroutine

‘I2C_READ_BYTE’ subroutine (lines 282-295) allows you to read one byte from the I2C bus. It’s a bit similar to the ‘I2C_WRITE_BYTE’ subroutine. We set the ‘bit_count’ value to 8 (lines 283, 284) then clear the ‘i2c_data’ register (line 285). Now we are ready to receive the data bits, which we do in the ‘I2C_READ_BIT’ block (lines 286-295). Firstly we shift the ‘i2c_data’ register at one bit to the left (line 287). I already explained in one of the previous tutorials why we should do the shift first. Then we set SCL low (line 288) to complete the preparation for the receiving of a new bit, and then we set SCL high (line 289), now we’re safe to read the value from the Slave device, as it will not change it while we hold SCL high. So we check the ‘sda’ bit of the GPIO register (line 290). If it is 1, then we set the LSB of the ‘i2c_data’ register (line 291), otherwise this line is skipped, and the value of the bit remains 0. Then we decrement the ‘bit_count’ value and check if it’s 0 (line 292), If not, then we return to the ‘I2C_READ_BIT’ label (line 293), otherwise we consider that the reception is completed, we set SCL low (line 294) to finish the last pulse, and now we need to send either ACK or NACK which we do in one of the following subroutines.

;----------I2C send ACK----------------------

I2C_ACK

CALL SDA_LOW   ;Set SDA low to issue ACK condition

    CALL SCL_HIGH    ;Set SCL high to start the new pulse

    CALL SCL_LOW   ;Set SCL low to end the pulse

    CALL SDA_HIGH    ;Set SDA high to release the bus

    RETLW 0

;----------I2C send NACK----------------------

I2C_NACK

    CALL SDA_HIGH    ;Set SDA low to issue NACK condition

    CALL SCL_HIGH    ;Set SCL high to start the new pulse

    CALL SCL_LOW   ;Set SCL low to end the pulse

    RETLW 0

;-------------Delay subroutine--------------

DELAY        ;Start DELAY subroutine here  

MOVWF i    ;Copy the value to the register i

MOVWF j    ;Copy the value to the register j

DELAY_LOOP    ;Start delay loop

DECFSZ i, F    ;Decrement i and check if it is not zero

GOTO DELAY_LOOP   ;If not, then go to the DELAY_LOOP label

DECFSZ j, F    ;Decrement j and check if it is not zero

GOTO DELAY_LOOP   ;If not, then go to the DELAY_LOOP label

RETLW 0    ;Else return from the subroutine

    END

‘I2C_SEND_ACK’ subroutine (lines 297-302) as follows from its name, sends the acknowledgement bit after receiving the byte. As you remember after finishing the ‘I2C_READ_BYTE’ subroutine we left the SCL line low, so we can now change the SDA line. We set it low (line 298) and start the new pulse by setting SCL high (line 299). Then we end the pulse by setting SCL low (line 300) and release the SDA line by setting it high (line 301).

‘I2C_SEND_NACK’ subroutine (lines 304-308) sends the not-acknowledgement bit after receiving the byte. We set the SDA high (line 305) and start the new pulse by setting SCL high (line 306). Then we end the pulse by setting SCL low (line 307). The SDA is already high, so no actions with it are required.

Well, that’s all about the I2C implementation. There is the ‘DELAY’ subroutine below (lines 311-319) but it’s standard, so there’s nothing new to explain.

And finally we’re done! We’ve considered the whole program and know how it works. Now it’s time for some practical work.


Please assemble the device according to figure 10. Initially don’t connect the VCC line of the FM radio module to prevent its damage if you accidently set the supply voltage as 5V. SCL and SDA pins of the FM radio module are OK with the higher voltage, so you may keep them connected.

Now connect the PICKit to the USB port and configure its supply voltage. For some reason when I set the voltage lower than 4V, the PIC10F200 chip refused to be flashed. So I had to follow these steps:

  1. Disable powering of the device by the programmer.
  2. Disconnect the VCC pin of the FM module.
  3. Connect the VPP, ICSPDAT, and ICSPSCK wires from the programmer.
  4. Set the supply voltage as 4V.
  5. Enable powering of the device.
  6. Flash the microcontroller.
  7. Disconnect the VPP, ICSPDAT, and ICSPSCK wires of the programmer.
  8. Disable powering of the device.
  9. Connect the VCC pin of the FM module.
  10. Set the supply voltage as 3.25V.
  11. Enable powering of the device.

If you’ve made everything correctly, you should hear the loud noise in your earphones. Now try to press the button Down and hold it, the volume of the noise should decrease. Then do a short press on the Up or Down button, the FM radio chip will start seeking the station. If it can’t find anything, try to press the button several times. If still nothing, check the antenna. When you finally hear the station, leave it without pressing anything for more than 10 seconds, and then try to power off the device. The station and the volume level should remain the same.

For proper operation don’t forget to disconnect the VPP, ICSPDAT, and ICSPSCK wires of the programmer as they affect the device.

And now let me show you some diagrams that were made with my favorite Logic analyzer to let you see how the I2C communication looks in reality.

Figure 11. Reading from the EEPROM at the startup.
Figure 11. Reading from the EEPROM at the startup.
Figure 12. Writing the volume level to the FM radio chip at the startup.
Figure 12. Writing the volume level to the FM radio chip at the startup.

In figures 11 and 12 you can see the actual signals (SCL in line 1, and SDA in line 2). The logic analyzer decodes the signal without any problems, which means our code works correctly. The green circle represents the Start condition, the red circle is Stop condition. In the blue rectangles you can read the decoded values, and if you compare them with the code (‘READ_EEPROM’ and ‘SET_VOLUME’ parts) you can see that we have the same values that we sent, which is really good.

I should mention that a lot of microcontrollers have hardware module that implement the I2C interface, so you don’t need to do all these bit banging functions, you just set the Slave device address, the data you want to send (or number of bytes you want to read), and then the module will do everything for you - issue the Start and Stop conditions, transmit/receive the data, check the ACK bit and so on. But in our program we can see what’s hidden inside this module, and the main purpose of this series of Tutorials is to reveal what’s hidden inside.


In conclusion, let me give you some statistics. We’ve studied another very popular interface - I2C. After a long break we finally learnt the new instruction INFSZ, and now we know 25 of 33. The program spends 235 words of the flash memory, and this is the longest one in this series.

As home work, I’d suggest you read the data sheet of the RDA5807M and try to change some other settings of the chip, it has a lot of parameters that affect its behavior. Also, you can write a special helper program to save the frequencies of the several stations into the EEPROM, and then run another (main) program in which you will move between these stations when pressing the Up and Down buttons instead of just blindly seeking across the band.

Related Tutorials


Terms Used

Make Bread with our CircuitBread Toaster!

Get the latest tools and tutorials, fresh from the toaster.

What are you looking for?