I2C FM Radio - Part 15 Microcontroller Basics (PIC10F200)
Published
Note: Microchip recently made some changes and their newer IDE versions use the XC8 compiler for Assembly, whereas all sample code here is created for the MPASM compiler. However, the sample code has been ported to XC8 by user tinyelect on our Discord channel (to whom we are extremely grateful!) and the XC8 version of the code is at the bottom of this tutorial for your reference. To see the changes needed to switch from MPASM to XC8, please check out the process that he shared.
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.
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).
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)
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)
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).
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)
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).
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.
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.
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 write 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).
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:
- Disable powering of the device by the programmer.
- Disconnect the VCC pin of the FM module.
- Connect the VPP, ICSPDAT, and ICSPSCK wires from the programmer.
- Set the supply voltage as 4V.
- Enable powering of the device.
- Flash the microcontroller.
- Disconnect the VPP, ICSPDAT, and ICSPSCK wires of the programmer.
- Disable powering of the device.
- Connect the VCC pin of the FM module.
- Set the supply voltage as 3.25V.
- 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.
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.
Check Yourself
23 Questions
Get the latest tools and tutorials, fresh from the toaster.