STM8 + ASSEMBLER: Драйвер FM-приемника RDA5807m для микроконтроллера STM8S103F3 (обновлено 15 июля)

разделы: STM8 , RDA5807M , дата: 29 декабря 2019г.

В статье пошагово описывается процесс написания драйвера для FM-приемника RDA5807m, где в качестве микроконтроллера используется STM8S103F3, а в качестве языка программирования - ассемблер со средой программирования STVP.

Структурно статью можно разделить три части. С одной стороны это статья об ассемблере STM8, в частности здесь имеются замечания об использовании косвенной адресации и использования указателя стека в качестве индексного регистра. Собственно, вся статья построена на ассемблерном коде STM8. С другой стороны, рассматривается периферия STM8, в частности в статье описывается создание UART приёмо-передатчика для микроконтроллера STM8. Это может быть использовано для управления коммуникационными модулями с UART интерфейсом, навроде: esp8266, esp32, rda5981 и пр. С третьей стороны, в статье главной темой является RDA5807m. Здесь ему, правда, уделяется всего одна глава, т.к. сам по себе чип несложный.

Совершенно другое дело - система передачи данных RDS (Radio Data System). Я смог добиться лишь декодирования RDS - текста. Это восемь символов латиницей, через которые передается название станции. К сожалению, я не смог найти станцию которая бы передавала текущее время, но я все-равно планирую рассказать об этой возможности во второй статье (устарело, сейчас чтение RDS уже реализовано).

Данная статья является первой частью, в ней рассматривается лишь минимальный драйвер RDA5807m, который годится лишь для проверки модуля. Полноценный драйвер я планирую описать во второй статье, кроме того, там должно быть много материала по RDS. Это будут выдержки из стандарта: "EN50067. Specification of the radio data system (RDS) for VHF/FM sound broadcasting in the frequency range from 87,5 to 108,0 MHz. April 1998. с описанием формата, а также логи принятых данных.

Кого-то может смутить использование ассемблера в наше время. Лично я считаю развитие темы интернета вещей и различных SoC постепенно вытеснит низкоуровневое программирование в принципе, поэтому данная статья - это реверанс в сторону хардкорного программирования.

Полезные материалы по теме статьи:

  1. Статья: "STM8S + SDCC: Программирование БЕЗ SPL. Интерфейсы: UART в режиме передатчика, АЦП в режиме однократного замера, I2C в режиме мастера на примере DS1307/DS3231"
  2. Статья: "STM8 + STVD + ASSEMBLER: Быстрый старт"
  3. Обзорная статья по RDA5807m: "Arduino: FM-радиомодуль на микросхеме RDA5807m"

Содержание:

I. Реализация командного интерфейса посредством UART

  1. Схема подключения
  2. Базовый проект, реализация программы эхо/echo для UART интерфейса
  3. Использование косвенной адресации
  4. Отладка прерывания
  5. Реализация командного интерфейса

II. Драйвер управления RDA5807m

  1. Минимальный драйвер для управления FM-приемником RDA5807m
  2. Чтение частоты станции и переключение тюнера на заданную частоту

III. Драйвер с переключением диапазонов и интервалов частот

  1. Вводная часть
  2. Порядок работы с драйвером
  3. Использование ОЗУ дайвером
  4. Длинные переходы и реализация оператора case на ассемблере STM8
  5. Реализация переключения диапазонов и интервалов
  6. Функции шумоподавления

IV. Прием RDS данных (добавлено 25 июня 2020г.)

  1. Работа над ошибками (борьба с аппаратным багом программными средствами)
  2. Что такое RDS и как его читать с помощью RDA5807m
  3. Преобразование даты из MJD формата в дни, года и месяцы
  4. Математическая библиотека для 32/24 битных операций. Операции сложения, вычитания и умножения
  5. Математическая библиотека для 32/24 битных операций. Операция деления
  6. Подпрограмма преобразования даты
  7. Реализация чтения RDS сообщений

V. Подключение энкодера и дисплея к драйверу (добавлено 15 июля 2020г.)

  1. Рефакторинг кода драйвера

Посмотреть исходники, или скачать скомпилированные прошивки можно с портала GitLab по следующей ссылке: https://gitlab.com/flank1er/stm8_rda5807m.

1) Схема подключения

Для сборки устройства, нам понадобятся: сам модуль RDA5807m, плата с микроконтроллером STM8S103F3P6 и модуль с USB-UART преобразователем. Я буду использовать UART адаптер на чипе ft232rl, но может быть использован любой другой. В данном случае можно использовать USB-UART адаптеры с 5 вольтовой логикой, т.к. для S-серии это штатное напряжения. Но чип на плате обязательно(!) должен работать от напряжения 3.3 Вольт. Общая схема подключения модулей друг к другу выглядит так:

Здесь два независимых источника питания: а) питание 3.3 Вольт с программатора ST-LINKv2 подается на плату STM8S103F3P6 и на FM-приемник RDA5807m; б) USB-UART преобразователь ft232rl питается независимо от USB порта компьютера к которому он подключен. "Земля" преобразователя ft232rl соединяется с "землей" программатора ST-LINKv2.

К FM-приемнику RDA5807m подключаются также плеерные наушники с входным импедансом 32 Ом и FM антенна (обычный провод длиной 20 см).

2) Базовый проект, реализация программы эхо/echo для UART интерфейса

Написание кода, начнем не с FM приемника, а с реализации коммуникационного интерфейса посредством UART между чипом STM8 и компьютером. Как написать UART-передатчик я писал в прошлогодней статье: "UART1 передатчик со скоростью 921600 baud". За исключением настройки скорости работы порта, там нет ничего сложного. Но сейчас нам еще понадобится UART-приёмник. Сделать его оказалось несколько сложнее, чем передатчик или тот же UART приемник на AVR. Коммуникационный интерфейс посредством UART может быть использован не только в данном проекте, но также для управления различными коммуникационными модулями WiFi и Bluetooth c управлением через UART. Поэтому мне показалось, что тема заслуживает подробного рассмотрения.

Схема подключения пока должна быть такой:

Подключать модуль RDA5807m пока не надо, т.к. для индикации работы главного цикла будет использоваться светодиод на PB5, а это SDA линия I2C интерфейса.

Приемник на UART интерфейсе делается на 18-ом прерывании STM8S, которое отвечает за входящее UART соединение. Вызов данного прерывания происходит при установке двух флагов: RXNE и OR.

RXNE - это "рабочий" флаг, установка которого говорит о том, что модуль UART принял байт данных, и он готов для считывания в регистре UART_DR. Данный флаг сбрасывается путем чтения регистра UART_DR.

OR - это флаг ошибки переполнения (OveRflow). Он устанавливается при поступлении новых данных на UART модуль, в то время, как предыдущие данные еще не были считаны с регистра UART_DR. Эти данные теряются. Установленный флаг OR сигнализирует об этом.

Алгоритм работы обработчика 18-го прерывания должен быть таким.

  1. При входе в прерывание сначала следует проверить OR-флаг. Если он установлен, следует сбросить его последовательным чтением регистров UART_DR и UART_SR(именно в таком порядке).
  2. Если OR-флаг не был установлен, следовательно прерывание было вызвано по RXNE-флагу. Его сброс осуществляется чтением UART_DR регистра. Далее с полученными данными производятся какие-то манипуляции, после чего следует выход из прерывания.

Теперь, действуя в соответствии с главами 1, 2, 3 и 4, моей статьи "STM8 + STVD + ASSEMBLER: Быстрый старт", нужно создать шаблонный проект в STVD.

В файле main.asm пусть у нас будет следующая программа:

stm8/
    #include "STM8S103F.inc"
    extern delay, uart1_print_str, uart1_print_num,uart1_print_char

LED equ 5
LEN equ 10
EOL equ LEN         ; =Zero always
;-------- Variables ----------------------
STR equ 0                   ; buffer[10bytes]
INDEX   cequ    {EOL+1} ; 1 byte
READY   cequ    {INDEX+1}   ; 1 byte

    segment 'rom'
.main
    ;----------- Setup Clock ----------------------
    ; Setup fHSI = 16MHz
    clr CLK_CKDIVR
    ; Enable UART and turn off other  peripherals   
    mov CLK_PCKENR1, #0
    mov CLK_PCKENR2, #0
    bset CLK_PCKENR1, #3    ; enable UART1
    ;----------- Setup GPIO -----------------------
    bset PB_DDR, #LED       ; PB_DDR|=(1<<LED)
    bset PB_CR1, #LED       ; PB_CR1|=(1<<LED)
    ;----------- Setup UART1 ----------------------
    ; Clear
    clr UART1_CR1
    clr UART1_CR2
    clr UART1_CR3
    clr UART1_CR4
    clr UART1_CR5
    clr UART1_GTR
    clr UART1_PSCR
    ; Setup UART1, set 115200 Baud Rate
    bset UART1_CR1, #5      ; set UARTD, UART1 disable
    ; 9600 Baud Rate
    ;mov UART1_BRR2, #0x03
    ;mov UART1_BRR1, #0x68
    ; 115200 Baud Rate
    mov UART1_BRR2, #$0b
    mov UART1_BRR1, #$08
    ; 230400 Baud Rate
    ;mov UART1_BRR2, #0x05
    ;mov UART1_BRR1, #0x04
    ; 921600 Baud Rate
    ;mov UART1_BRR2, #0x01
    ;mov UART1_BRR1, #0x01
    ; Trasmission Enable
    bset UART1_CR2, #3      ; set TEN, Transmission Enable
    bset UART1_CR2, #2      ; set REN, Receiver Enable
    bset UART1_CR2, #5      ; set RIEN, Enable Receiver Interrupt 
    ; enable UART1
    bres UART1_CR1, #5      ; clear UARTD, UART1 enable
    ;------------- End Setup ---------------------
    clr EOL                 ;set NULL/EOL
start:
    clr INDEX               ; INDEX=0
    clr READY               ; READY=0

    ; let's go...
    rim                     ; enable Interrupts

mloop:
    ; receive string
    btjf READY,#0,main_no_receie
    ; print("count: ")
    ldw x, #msg_count
    call uart1_print_str
    clrw x
    ld a, INDEX
    ld xl,a
    call uart1_print_num    ; print count of symbols
    ldw x,#msg_line
    call uart1_print_str
    ldw x,#STR
    call uart1_print_str    ; print received string
    ld a, #$a
    call uart1_print_char
    jra start
main_no_receie:
    bcpl PB_ODR, #LED       ; PB_ODR^=(1<<LED)
    ldw x,#500              ; delay(500ms)
    call delay
    ldw x,#msg_line
    jp mloop

msg_count:
    STRING "count: "
    DC.B $00
msg_line:
    DC.B $0a
    STRING "line: "
    DC.B $00

    end

Здесь в начале идет переключение рабочей частоты fCPU на 16MHz. Т.к. на плате отсутствует кварц, то чип работает от внутреннего генератора - HSI. Далее идет включение GPIO_PB5 на выход, после чего следует код инициализации UART. Там мы указываем рабочую частоту 115200, включаем прием и передачу на UART и активируем прерывание на прием, т.е. INT_18. Далее следует главный цикл. В нем, если была получена новая строка, на выход подается полученная строка, и печатается число символов, которое было принято. Символы окончания строки при этом не подсчитываются.

После печати сообщения, если такое входные данные поступали, идет переключение светодиода и далее выполняется функция задержки на 500ms. Если данные будут поступать быстрее чем с интервалом в 500ms, то они будут теряться.

Про формат входных данных поговорим потом, пока продолжаем создавать базовый проект. Для этого в проект добавим файл utils.asm c функцией задержки на пустом цикле:

stm8/
    INTEL
    segment 'rom'
    ;----------- delay ---------------------------------
    ; input parameter: X - register
.delay:
    ldw y, #0fa0h      ; =4000
delay_loop:
    subw y,#1
    jrne delay_loop
    decw x
    jrne delay
    ret
    end

Далее добавляем файл uart1.asm с функциями печати символа, строки и целого неотрицательного числа:

stm8/
    INTEL
    #include "STM8S103F.inc"
    segment 'rom'

    ; ----------- print uint8_t ------------------------
    ; input parameter: X
.uart1_print_num:
    ldw y,sp
    push #0
uart1_print_num_loop:
    ld a, #10
    div x,a
    add a,#30h
    push a
    tnzw x
    jrne uart1_print_num_loop
    ldw x,sp
    incw x
    call uart1_print_str
    ldw sp,y
    ret

    ; ----------- print string -------------------------
    ;  input parameter:  X 
.uart1_print_str:
    ld a,(x)
    jreq uart1_str_exit
uart1_print_str_wait:
    btjf UART1_SR, #7, uart1_print_str_wait     ;wait if UART_DR is full yet (TXE == 0)
    ld UART1_DR, a
    incw x
    jra uart1_print_str
uart1_str_exit:
    ret

    ; ----------- send char to UART1 -------------------
    ;  input parameter: A
.uart1_print_char:
    btjf UART1_SR, #7, uart1_print_char     ;wait if UART_DR is full yet (TXE == 0)
    ld UART1_DR, a
    ret
    end

И все самое интересное будет в файле irq.asm:

stm8/
    INTEL
    extern main
    #include "mapping.inc"
    #include "STM8S103F.inc"

LEN equ 10
EOL equ LEN         ; =Zero always
;-------- Variables ----------------------
STR equ 0                   ; buffer[10bytes]
INDEX   cequ    {EOL+1} ; 1 byte
READY   cequ    {INDEX+1}   ; 1 byte

    segment 'rom'

reset.l
    ; initialize SP
    ldw X,#03ffh
    ldw SP,X
    jp main
    jra reset

    interrupt NonHandledInterrupt
NonHandledInterrupt.l
    jra NonHandledInterrupt
    iret 

UART_RX_IRQ.l:
    btjt UART1_SR,#3,CLEAR_OR_FLAG      ; if OR flag is set
    ld a,UART1_DR                       ; get received byte and clear RXNE flag
    cp a,#0dh                            ; if received char == CR
    jreq NULL_Terminate
    cp a,#0ah                            ; if received char == '\n'
    jreq NULL_Terminate
    ld yl,a                             ; store received char
    ld a,INDEX                          ; get index
    cp a,#LEN                           ; if index is over  
    jreq QUIT
    ld a,yl                             ; restore received char      
    ld [EOL.w],a
    inc INDEX
    iret
CLEAR_OR_FLAG:                          ; if OR flag is set, then clear it
    ld a,UART1_DR
    ld a,UART1_SR
NULL_Terminate:
    clr [EOL]                           ; set NULL/EOL
    ; set READY flag for main loop
QUIT:
    bset READY,#0                       ; was received EOL
    iret

    MOTOROLA
    segment 'vectit'
    dc.l {$82000000+reset}                  ; reset
    dc.l {$82000000+NonHandledInterrupt}    ; trap
    dc.l {$82000000+NonHandledInterrupt}    ; irq0
    dc.l {$82000000+NonHandledInterrupt}    ; irq1
    dc.l {$82000000+NonHandledInterrupt}    ; irq2
    dc.l {$82000000+NonHandledInterrupt}    ; irq3
    dc.l {$82000000+NonHandledInterrupt}    ; irq4
    dc.l {$82000000+NonHandledInterrupt}    ; irq5
    dc.l {$82000000+NonHandledInterrupt}    ; irq6
    dc.l {$82000000+NonHandledInterrupt}    ; irq7
    dc.l {$82000000+NonHandledInterrupt}    ; irq8
    dc.l {$82000000+NonHandledInterrupt}    ; irq9
    dc.l {$82000000+NonHandledInterrupt}    ; irq10
    dc.l {$82000000+NonHandledInterrupt}    ; irq11
    dc.l {$82000000+NonHandledInterrupt}    ; irq12
    dc.l {$82000000+NonHandledInterrupt}    ; irq13
    dc.l {$82000000+NonHandledInterrupt}    ; irq14
    dc.l {$82000000+NonHandledInterrupt}    ; irq15
    dc.l {$82000000+NonHandledInterrupt}    ; irq16
    dc.l {$82000000+NonHandledInterrupt}    ; irq17
    dc.l {$82000000+UART_RX_IRQ}            ; irq18
    dc.l {$82000000+NonHandledInterrupt}    ; irq19
    dc.l {$82000000+NonHandledInterrupt}    ; irq20
    dc.l {$82000000+NonHandledInterrupt}    ; irq21
    dc.l {$82000000+NonHandledInterrupt}    ; irq22
    dc.l {$82000000+NonHandledInterrupt}    ; irq23
    dc.l {$82000000+NonHandledInterrupt}    ; irq24
    dc.l {$82000000+NonHandledInterrupt}    ; irq25
    dc.l {$82000000+NonHandledInterrupt}    ; irq26
    dc.l {$82000000+NonHandledInterrupt}    ; irq27
    dc.l {$82000000+NonHandledInterrupt}    ; irq28
    dc.l {$82000000+NonHandledInterrupt}    ; irq29

        END

Прошивка "весит" 401 байт. В принципе самым интересным здесь является обработчик 18-го прерывания UART_RX_IRQ. Но обо всем по порядку. Вначале обсудим формат данных. Они описываются в программе следующей структурой:

LEN equ 10
EOL equ LEN         ; =Zero always
;-------- Variables ----------------------
STR equ 0                   ; buffer[10bytes]
INDEX   cequ    {EOL+1} ; 1 byte
READY   cequ    {INDEX+1}   ; 1 byte

Данный код у меня продублирован в файлах irq.asm и main.asm, хотя правильнее было бы вынести его в отдельный inc файл. Но чтобы не плодить сущности, пока так.

Распределение используемой программной памяти в соответствии с этой структурой выглядит так:

Здесь первые десять байт занимает буфер, в который будет помещаться принимаемая по UART строка. Буфер начинается с нулевого адреса ОЗУ, что очень удобно в том плане, что индекс массива будет являться и адресом ячейки памяти для этого элемента. Минусом такого расположения массива является то, что при ошибке приводящей к переполнению буфера, данные расположенные следом за буфером будут запираться, что приведет к неопределенному поведению программы. На это могу только сказать, что не надо писать программы с ошибками.

За буфером, по адресу 10, располагается константа EOL. Значение этой ячейки памяти всегда равно нулю. Обнуление этой ячейки памяти производится в секции инициализации файла main.asm (выделено красным):

    ;------------- End Setup ---------------------
    clr EOL                 ;set NULL/EOL
start:
    clr INDEX               ; INDEX=0
    clr READY               ; READY=0

    ; let's go...
    rim                     ; enable Interrupts

EOL - это константа которая используется в двух случаях. Во-первых, она выполняет роль признака конца строки, когда размер входной строки достигает максимума, т.е. 10 байт. Во-вторых она выполняет роль старшего байта при косвенной адресации к элементам массива. Поэтому значение EOL должно быть всегда равно нулю. Т.о. 256 байт - это максимальный предел строки обусловленный алгоритмом.

INDEX - это переменная хранящая текущий индекс массива. Одновременно это еще и адрес ячейки памяти с элементом массива.

READY - это флаговая переменная сигнализирующая о завершении приема строки, когда поступил признак окончания строки, или когда был превышен размер буфера. Флагом выступает младший бит, хотя флагом можно было бы сделать сам байт и проверять его на ноль.

Теперь разберем код обработчика прерывания UART_RX_IRQ.

В самом начале идет проверка на установленный флаг OR. Если он установлен, то весь "концерт" отменяется, и мы переходим к сбросу этого флага, с последующим выходом из прерывания:

UART_RX_IRQ.l:
    btjt UART1_SR,#3,CLEAR_OR_FLAG      ; if OR flag is set

Если же флаг OR не установлен, то мы делаем вывод, что прерывание было вызвано установкой флага RXNE, это значит, что регистр UART_DR не пуст. Тогда считываем его значение, и тем самым сбрасываем флаг RXNE:

    ld a,UART1_DR                       ; get received byte and clear RXNE flag

Далее проверяем, не является ли полученное значение символом окончания строки - NL или CR. Можно сократить код обработчика прерывания на две инструкции, если выбрать какой-либо один символ окончания строки.

    cp a,#0dh                            ; if received char == CR
    jreq NULL_Terminate
    cp a,#0ah                            ; if received char == '\n'
    jreq NULL_Terminate

Если нет, то содержимое аккумулятора сохраняется в регистре yl, после чего в аккумулятор загружается значение индекса массива, и он сравнивается с максимально допустимой длинной LEN:

    ld yl,a                             ; store received char
    ld a,INDEX                          ; get index
    cp a,#LEN                           ; if index is over  
    jreq QUIT

Если индекс еще не поравнялся с максимально допустимой длинной строки(массива) LEN, то снова в аккумулятор загружаем полученное значение из UART_DR, и сохраняем его в массиве:

    ld a,yl                             ; restore received char      
    ld [EOL.w],a

После этого увеличиваем на единицу значение индекса, и выходим из обработчика прерывания:

    inc INDEX
    iret

3) Использование косвенной адресации

Вот с квадратными скобочками хотелось бы разобраться поподробнее. Косвенная адресация не самая быстрая, т.к. сначала нужно "добыть" адрес источника/получателя в то время как при индексной адресации процессору сразу подсовывается регистр содержащий нужный адрес. НО. Когда-то во-времена процессоров Z80,6800, 8080 это может быть и было справедливо. Теперь же у нас есть конвейер который как раз и занимается "добычей" нужных адресов. Кроме того, у нас мало регистров, а косвенная адресация позволяет избежать лишних операций с "перетасовкой" регистров. Сравните один и тот же код с косвенной и индексной адресацией:

Сохранение элемента массива через косвенную адресацию:

    ld a,yl    ; restore received char      
    ld [EOL.w],a

Тоже самое через индексную адресацию:

    clrw x
    ld xl,a
    ld a,yl     ; restore received char
    ld (x),a    ; store char to buffer

Также может вызвать вопрос, почему в операнде стоит адрес EOL, а не INDEX. Все дело в том, что STM8 наследник мотороловской архитектуры 68HC05 (код 68HC05 бинарно совместим с ST7 и STM8), и "по наследству" получила Big-Endian порядок байтов. Т.е. по младшему адресу идут старшие байты, а по старшему адресу идут младшие байты. Я уже обращал на это внимание в статье " Запись в EEPROM средствами СOSMIC":

Первое, что здесь бросается в глаза, то что двухбайтные числа пишутся в обычном, а не перевёрнутом порядке, когда пишется сначала младший байт, а потом старший.

В документации про программированию - "Programming Manual PM0044" есть замечательная иллюстрация того, как работает инструкция LD в случае использования косвенной индексации:

В нашем случае по адресу EOL записан всегда ноль, следом идет INDEX, следовательно обращение будет по адресу записанному в INDEX. Такая вот логика.

4) Отладка прерывания

Если вы планируете вносить свои изменения в обработчик прерывания, то вам скорее всего потребуется его отлаживать. У этого процесса есть рад особенностей.

Во-первых, для отладки следует посылать в микроконтроллер по одной литере, иначе вы установите флаг OR. Поэтому для отладки, в терминальной программе следует убрать символ окончания стоки:

В самой программе следует поставить точку останова в начале прерывания:

После этого можно запустить отладку, затем запустить программу на выполнение (восклицательный знак на панели отладки) и следом послать один символ через терминальную программу. Нас должно "выбросить" в режим трассировки. В окне "Memory" следует указать адрес 0x0:

После этого можно приступать к трассировке до выхода из прерывания. Если вам нужно лишь посмотреть результат в окошке "Memory" то вместо трассировки можно нажать значок "Continue" (подчеркнуто синим) и послать следующий символ через терминальную программу. Нас снова "выкинет" в режим трассировки, в окошке "Memory" изменённые байты будут выделены жирным шрифтом:

И так шаг за шагом. Если нужно будет послать символ окончания строки, то выбираете соответствующий символ в терминальной программе и посылаете пустую строку. Если нужно отладить флаг OR, то посылаете несколько символов за раз. В принципе, не сложно.

В общем виде, лог работы программы пока выглядит так:

Здесь, строки которые не вмещаются в 10 символов, обрезаются.

5) Реализация командного интерфейса

Теперь, когда у нас есть интерфейс, через который мы можем посылать команды на микроконтроллер, нам нужны будут функции (подпрограммы)для обработки строк, чтобы эти команды были понятны управляющей программе микроконтроллера. Нам нужны будут функции сравнения строк и извлечения чисел из строки. Для тестирования этих подпрограмм, мы напишем что-то вроде прототипа управляющей программы, которая будет принимать по UART эти команды, и в ответ она будет выполнять какие-то действия.

Вернемся к нашему проекту. Файл irq.asm мы оставим без изменения, а остальные модули придется доработать. Начнем с малого, файл uart1.asm:

stm8/
    INTEL
    #include "STM8S103F.inc"
    segment 'rom'

    ; ----------- print uint8_t ------------------------
    ; input parameter: X
.uart1_print_num:
    pushw x
    pushw y
    push a
    ldw y,sp
    push #0
uart1_print_num_loop:
    ld a, #10
    div x,a
    add a,#30h
    push a
    tnzw x
    jrne uart1_print_num_loop
    ldw x,sp
    incw x
    call uart1_print_str
    ldw sp,y
    pop a
    popw y
    popw x
    ret

    ; ----------- print string -------------------------
    ;  input parameter:  X 
.uart1_print_str:
    pushw x
    push a
uart1_print_str_start
    ld a,(x)
    jreq uart1_str_exit
uart1_print_str_wait:
    btjf UART1_SR, #7, uart1_print_str_wait     ;wait if UART_DR is full yet (TXE == 0)
    ld UART1_DR, a
    incw x
    jra uart1_print_str_start
uart1_str_exit:
    pop a
    popw x
    ret

    ; ----------- send char to UART1 -------------------
    ;  input parameter: A
.uart1_print_char:
    btjf UART1_SR, #7, uart1_print_char     ;wait if UART_DR is full yet (TXE == 0)
    ld UART1_DR, a
    ret
    end

Здесь изменения минимальны, и они заключаются в сохранении в стеке регистров изменяемых подпрограммой (выделено красным). За счет этого общий код получается более компактным, т.к. пропадает необходимость восстанавливать значения регистров при каждом вызове этих подпрограмм.

Следующий файл - это utils.asm

stm8/
    segment 'rom'
    ;----------- compare first two symbols from buffer --------------------------
    ; input parameter: buffer adr=0x0, symbols sp+4,sp+5
.check_cmd
    push a
    clrw x              ; buffer address = 0x0
    ld a,($5,sp)
    cp a,(x)            ;  check first symbol
    jrne  check_notOk
    incw x
    ld a,($4,sp)
    cp a,(x)            ; check second symbol
check_notOk:
    pop a
    popw x              ; get return address
    addw sp,#2          ; restore SP
    jp (x)              ; ret

    ;----------- delay ---------------------------------
    ; input parameter: X - register
.delay:
    pushw x
    pushw y
delay_start
    ldw y, #4000      ; 1ms
delay_loop:
    subw y,#1
    jrne delay_loop
    decw x
    jrne delay_start
    popw y
    popw x
    ret
    end

Здесь у нас подпрограмма delay с добавленным блоком push/pop для сохранения регистров. Также сюда была добавлена подпрограмма check_cmd, которая проверяет двухсимвольную команду на соответствие шаблону. Двухсимвольные команды представляют большинство управляющих команд. Поэтом парсинг таких команд я решил вынести в отдельную подпрограмму chech_cmd. Примерами таких команд могут быть: v+, s-, d+, ?f и т.д. Подпрограмма принимает входные из стека, т.е. вызов подпрограммы будет выглядеть таким образом:

    push #'f'
    push #'='
    call check_cmd          ; check

Т.е. здесь мы проверяем начало входящего буфера на соответствие команде "f=". В качестве результата подпрограмма возвращает значение Z-флага. Поэтому при входе в подпрограмму, выполняем сохранение регистра А в стеке и обнуление регистра X:

    push a
    clrw x              ; buffer address = 0x0

Регистр Х содержит адрес буфера. Далее мы проверяем первый символ буфера на соответствие шаблону:

    ld a,($5,sp)
    cp a,(x)            ;  check first symbol
    jrne  check_notOk

Если проверка успешная, то проверяем второй символ:

    incw x
    ld a,($4,sp)
    cp a,(x)            ; check second symbol

Z-флаг который установился в результате выполнения последней инструкции "cp a,(x)" будет возвращен в качестве результата работы подпрограммы. Далее мы восстанавливаем регистр A, выравниваем указатель стека SP, и выходим из подпрограммы по адресу возврата извлеченного из стека:

    pop a
    popw x              ; get return address
    addw sp,#2          ; restore SP
    jp (x)              ; ret

Понятно, что при таком способе выхода из подпрограммы (т.е. через инструкцию jp вместо ret), сохранить значение регистра Х не получится.

Ок, посмотрим, что у нас в файле mian.c:

stm8/
    #include "STM8S103F.inc"
    extern delay,uart1_print_str,uart1_print_num,uart1_print_char,strcmp,strnum
    extern strfrac,check_cmd

print_nl MACRO
    ld a, #$a
    call uart1_print_char
    MEND
print_str MACRO msg
    ldw x, #msg
    call uart1_print_str
    MEND

LED equ 5
LEN equ 10
EOL equ LEN         ; =Zero always
;-------- Variables ----------------------
STR equ 0                   ; buffer[10bytes]
INDEX   cequ    {EOL+1} ; 1 byte
READY   cequ    {INDEX+1}   ; 1 byte

    segment 'rom'
.main
    ;----------- Setup Clock ----------------------
    ; Setup fHSI = 16MHz
    clr CLK_CKDIVR
    ; Enable UART and turn off other  peripherals   
    mov CLK_PCKENR1, #0
    mov CLK_PCKENR2, #0
    bset CLK_PCKENR1, #3    ; enable UART1
    ;----------- Setup GPIO -----------------------
    bset PB_DDR, #LED       ; PB_DDR|=(1<<LED)
    bset PB_CR1, #LED       ; PB_CR1|=(1<<LED)
    ;----------- Setup UART1 ----------------------
    ; Clear
    clr UART1_CR1
    clr UART1_CR2
    clr UART1_CR3
    clr UART1_CR4
    clr UART1_CR5
    clr UART1_GTR
    clr UART1_PSCR
    ; Setup UART1, set 115200 Baud Rate
    bset UART1_CR1, #5      ; set UARTD, UART1 disable
    ; 9600 Baud Rate
    ;mov UART1_BRR2, #0x03
    ;mov UART1_BRR1, #0x68
    ; 115200 Baud Rate
    mov UART1_BRR2, #$0b
    mov UART1_BRR1, #$08
    ; 230400 Baud Rate
    ;mov UART1_BRR2, #0x05
    ;mov UART1_BRR1, #0x04
    ; 921600 Baud Rate
    ;mov UART1_BRR2, #0x01
    ;mov UART1_BRR1, #0x01
    ; Trasmission Enable
    bset UART1_CR2, #3      ; set TEN, Transmission Enable
    bset UART1_CR2, #2      ; set REN, Receiver Enable
    bset UART1_CR2, #5      ; set RIEN, Enable Receiver Interrupt 
    ; enable UART1
    bres UART1_CR1, #5      ; clear UARTD, UART1 enable
    ;------------- End Setup ---------------------
    clr EOL                 ;set NULL/EOL
start:
    clr INDEX               ; INDEX=0
    clr READY               ; READY=0
    ; let's go...
    rim                     ; enable Interrupts
mloop:
    ; receive string
    btjt READY,#0,case
    jp blink
case:
    ; CHECK: if was recived "mute" command
    clrw x
    ldw y,#cmd_mute
    call strcmp
    jrne unmute
    jp print_command
    ; CHECK: if was recived "unmute" command
unmute:
    ;clrw x
    ldw y,#cmd_unmute
    call strcmp
    jrne freq
    jp print_command
freq:
    ; CHECK: if was recived "f=NUM.NUM" command
    push #'f'
    push #'='
    call check_cmd          ; check
    jrne help               ; if not "f=" then goto help
    ldw x,#02
    call strnum             ; get integer part
    subw sp,#2
    ld ($1,sp),a            ; (sp+1)=integer part
    call strfrac            ; get fractional part
    ld ($2,sp),a            ; (sp+2)=fractional part
    print_str msg_freq      ; print "freq="
    ld a,($1,sp)
    clrw x
    ld xl,a                 ; x=interger part
    call uart1_print_num    ; print x
    ld a, #'.'
    call uart1_print_char   ; print dot
    ld a,($2,sp)
    ld xl,a                 ; x=fractional part
    call uart1_print_num    ; print x
    print_nl                ; NewLine
    addw sp,#2
help:
    ; CHECK: if was recived "?" command
    clrw x                  ; if "v="
    ld a,(x)
    cp a,#'?'
    jrne help_2
    incw x
    ld a,(x)
    cp a,#0
    jrne help_2
    print_str msg_help
help_2:
    clrw x
    ldw y,#cmd_help
    call strcmp
    jrne volume
    print_str msg_help
volume:
    ; CHECK: if was recived "v=NUM" command
    push #'v'
    push #'='
    call check_cmd          ; check
    jrne next               ; if not "v=" then goto next
    ldw x,#02
    call strnum             ; convert string to number
    print_str msg_volume    ; print "volume="
    clrw x
    ld xl,a                 ; x=strnum
    call uart1_print_num    ; print x
    print_nl                ; NewLine
next:
    jp start

print_command:
    print_str msg_command
    clrw x
    call uart1_print_str
    print_nl
    jp start
blink:
    bcpl PB_ODR, #LED       ; PB_ODR^=(1<<LED)
    ldw x,#500              ; delay(500ms)
    call delay
    jp mloop

msg_freq:
    STRING "freq=",$00
msg_volume:
    STRING "volume=",$00
msg_command:
    STRING "command: ",$00
cmd_mute:
    STRING "mute",$00
cmd_help:
    STRING "help",$00
cmd_unmute:
    STRING "unmute",$00
msg_help:
    STRING "Available commands:",$0A
    STRING "* s-/s+       - seek down/up with band wrap-around",$0A
    STRING "* v-/v+       - decrease/increase the volume",$0A
    STRING "* b-/b+       - bass on/off",$0A
    STRING "* d-/d+       - debug print on/off",$0A
    STRING "* mute/unmute - mute/unmute audio output",$0A
    STRING "* ww/jp/ws/es - change band to: World Wide/Japan/West Europe/East Europe",$0A
    STRING "* 50MHz/65NHZ - change low edge to 50MHz or 65MHz for East Europe band",$0A
    STRING "* c-/c+       - change space: 100kHz/200kHz/50kHz/25kHz",$0A
    STRING "* rst         - reset and turn off",$0A
    STRING "* on          - Turn On",$0A
    STRING "* ?f          - display currently tuned frequency",$0A
    STRING "* ?q          - display RSSI for current station",$0A
    STRING "* ?v          - display current volume",$0A
    STRING "* ?m          - display mode: mono or stereo",$0A
    STRING "* v=num       - set volume, where num is number from 0 to 15",$0A
    STRING "* t=num       - set SNR threshold, where num is number from 0 to 15",$0A
    STRING "* b=num       - set soft blend threshold, where num is number from 0 to 31",$0A
    STRING "* f=freq      - set frequency, e.g. f=103.8",$0A
    STRING "* ?|help      - display this list",$0A,$00,
    end

Здесь весь код можно разделить на четыре части. Первая часть это блок макроопределений и директив. В нем определены два макроса:

print_nl MACRO
    ld a, #$a
    call uart1_print_char
    MEND
print_str MACRO msg
    ldw x, #msg
    call uart1_print_str
    MEND

Макрос print_nl печатает символ новой строки - "NL". Макрос print_str печатает строку. В качестве параметра он принимает адрес этой строки. Оба макроса НЕ восстанавливают регистры которые они изменяют.

Следующий блок расположен между метками main и start. Он содержит инициализацию периферии, в том числе UART модуля.

Блок между метками mloop и print_command - реализует главный цикл который работает по принципу оператора case. Т.е. если поступила такая команда, то делаем то, если другая, то делаем это и т.д.

В конце расположен блок символьных констант, которые используются для диалогового режима с пользователем.

Хочу обратить внимание на последнее сообщение msg_help которое выводится на команды "?" или "help":

Это сообщение стоит нам около 1 Кбайта места на флеш-памяти, и оно показывает, какие команды для работы c RDA5807 мы будем реализовывать. Если у вас не будет хватать места на флеш-памяти, это сообщение можно будет удалить, освободив около одного килобайта памяти. И это никак не скажется на функциональности прошивки. Остальные сообщения в принципе можно будет засунуть на EEPROM.

Команды и способы их обработки можно условно разделить на три группы. Первая - это команды состоящие из пары символов, таких большинство. В этом случае мы: а) парсим входящий буфер с помощью подпрограммы check_cmd; б) и если после отработки check_cmd Z-флаг оказывается установлен, то запускаем подпрограмму которая будет выполнять соответствующее действие.

Вторая группа команд - это длинное слово. Т.е. это команды: mute, unmute, help, rst, 50MHz, 65MHz. В общем случае их обработка выглядит следующим образом. Вызывается подпрограмма сравнения двух строк, в качестве параметров подпрограмме передаются адреса входящего буфера, и конкретной команды. Затем, по состоянию Z-флага идет выполнение этой команды, или переход к проверке следующей команды.

    clrw x
    ldw y,#cmd_help
    call strcmp
    jrne volume
    print_str msg_help

Третья группа команд, это команда с параметром. Через них передаются громкость звука или частоту станции. Соответственно нужен подпрограммы преобразования строки в целое число, а т.к. частота FM-станции передается дробным числом, потребуется подпрограмма чтения дробной части. В программе целая часть числа и дробная считываются отдельно. Например, если частота станции 102.8, то на выходе получим целые числа 102 и 8. Обработка команды передачи частоты станции выглядит так:

freq:
    ; CHECK: if was recived "f=NUM.NUM" command
    push #'f'
    push #'='
    call check_cmd          ; check
    jrne help               ; if not "f=" then goto help
    ldw x,#02
    call strnum             ; get integer part
    subw sp,#2
    ld ($1,sp),a            ; (sp+1)=integer part
    call strfrac            ; get fractional part
    ld ($2,sp),a            ; (sp+2)=fractional part
    print_str msg_freq      ; print "freq="
    ld a,($1,sp)
    clrw x
    ld xl,a                 ; x=interger part
    call uart1_print_num    ; print x
    ld a, #'.'
    call uart1_print_char   ; print dot
    ld a,($2,sp)
    ld xl,a                 ; x=fractional part
    call uart1_print_num    ; print x
    print_nl                ; NewLine
    addw sp,#2

В приведенном выше примере, регистр SP используется в качестве индексного. Таким способом в стеке сохраняются временные данные, в данном случае, целая и дробная часть числа. В этом имеется подводный камень: Вы НИКОГДА не должны размещать ваши данные "ниже" указателя стека SP, потому что, если во время исполнения вашего кода случится прерывание, ваши данные затрутся в тот же миг, т.к. при входе в обработчик прерывания автоматически в стеке сохраняются все РОН. С одной стороны архитектура STM8 защищает вас от хранения данных под стеком: к индексному регистру можно только прибавить. Но если вы будете использовать нулевое смещение, т.е. что-то вроде: "ld ($0,sp), a", то это и будет ниже уровня стека. Эти данные будут уязвимыми. В STM8 указатель стека указывает на свободную ячейку памяти. Поэтому, чтобы адресовать к сохраненным в стеке данными, при индексной адресации следует использовать в операнде (num,sp), где num больше нуля.

Последние три подпрограммы размещаются в файле string.asm:

stm8/
    INTEL
    segment 'rom'
    ;----------- convert string to number, integer part -------------
    ; parameters: X - input string; A - output num
.strnum:
    pushw x
    pushw y
    clrw y              ; y=0
strnum_loop:
    ld a,(x)
    jreq strnum_quit    ; if a==0 then EOL
    cp a,#'.'
    jreq strnum_quit    ; if end of interger part
    cp a,#39h
    jrugt strnum_next   ; if not digit then skip
    sub a,#30h
    jrslt strnum_next   ; if not digit then skip
    push a
    ld a,#10
    mul y,a             ; y = y * 10
    ld a,yl             ; y = y % 256
    add a,(1,sp)        ; y = y + a
    ld yl,a
    pop a
strnum_next:
    incw x
    jra strnum_loop
strnum_quit:
    ld a,yl             ; return y
    popw y
    popw x
    ret

    ;----------- eject fraction part from string  -------------
    ; parameters: X - input string; A - output num
.strfrac:
    ld a,#'.'
    cp a,(x)
    jrne strfrac_next
    pushw x
    incw x
    callr strnum
    popw x
    ret
strfrac_next:
    tnz (x)
    jreq strfrac_quit
    incw x
    jra strfrac
strfrac_quit
    clr a
    ret

    ;----------- compare two strings ---------------------------------
    ; input parameter: X - first string, Y = second string
    ; output - Accumulator
.strcmp:
    pushw x
    pushw y
strcmp_start:
    ld a,(y)
    jrne strcmp_nozero
    ld a,(x)
strcmp_quit_notok:
    popw y
    popw x
    ret
strcmp_nozero:
    cp a,(x)
    jrne strcmp_quit_notok
    incw x
    incw y
    jra strcmp_start
    end

Это подпрограммы сравнения строк, преобразования строки в целое число и выделения дробной части.

Подпрограмма сравнения двух строк самая простая. Вначале, элемент массива (строки) загружается в аккумулятор, и если он не равен нулю(т.е. не конец строки), то идет переход на метку "strcmp_nozero".

    ld a,(y)
    jrne strcmp_nozero

А если он равен нулю, то в аккумулятор загружается элемент второй строки:

    ld a,(x)

Z-флаг который выставляется в результате выполнения этой инструкции, возвращается в качестве результата выполнения подпрограммы.

Если же загружаемый в аккумулятор элемент первой строки не равен нулю, то элементы массивов сравниваются друг с другом, и если они не равны друг-другу, то идет выход из подпрограммы, иначе цикл идет на следующую итерацию:

    cp a,(x)
    jrne strcmp_quit_notok
    incw x
    incw y
    jra strcmp_start

Подпрограмма преобразования строки в целое число - "strnum", работает следующим образом. 1) при инициализации результат приравнивается нулю. 2) далее в теле цикла, из строки начинают считываться числа в порядке слева направо. Символы которые не являются числами - игнорируются. Если алгоритм "натыкается" на символ точки или конца строки, то работа подпрограммы прекращается. 3) в теле цикла, результат предыдущей итерации умножается на 10, после чего к нему плюсуется считанное число. после этого запускается новая итерация. Должен заметить, что подпрограмма считывает только однобайтные числа. В нашем случае этого вполне достаточно.

Теперь рассмотрим подпрограмму по инструкциям. Регистр Y служит хранилищем общего результата. В самом начале он обнуляется:

    pushw x
    pushw y
    clrw y              ; y=0

В теле цикла, в аккумулятор загружается очередной символ строки, и если он равен нулю или является символом точки, то происходит выход из подпрограммы:

    ld a,(x)
    jreq strnum_quit    ; if a==0 then EOL
    cp a,#'.'
    jreq strnum_quit    ; if end of interger part

Затем проверяется диапазон символа. Если он не является числом, то он игнорируется. Для оптимизации, проверка на нижнюю границу диапазона совмещается с операцией преобразования символа в число. Т.е. из аккумулятора вычитается число 0х30:

    cp a,#39h
    jrugt strnum_next   ; if not digit then skip
    sub a,#30h
    jrslt strnum_next   ; if not digit then skip

После этого, содержимое регистра Y умножается на 10, и к нему прибавляется считанное число:

    push a
    ld a,#10
    mul y,a             ; y = y * 10
    ld a,yl             ; y = y % 256
    add a,(1,sp)        ; y = y + a
    ld yl,a
    pop a

В завершение, происходит переход на новый цикл итерации:

    incw x
    jra strnum_loop

Что касается подпрограммы извлечения дробного числа "strfrac", то она ищет в строке точку, после чего вызывает подпрограмму "strnum".

На данном этапе, наша прошивка весит 1602 байта, из которых около одного килобайта занимает занимает сообщение подсказки "msg_help".

6) Минимальный драйвер для управления FM-приемником RDA5807m

В этой главе мы подключим RDA5807m к микроконтроллеру STM8S103F3 и напишем минимальный по функционалу драйвер для работы с RDA5807m. С помощью этого драйвера можно будет включить FM-приёмник, перематывать станции и изменять громкость звучания. Такой драйвер сгодится для проверки работоспособности модуля, но не более. С точки зрения кода, он тем не менее включает в себя модуль работы с I2C модулем STM8, а также программный модуль работы с регистрами RDA5807m (запись/чтение).

Хочу напомнить, что RDA5807m имеет три I2C адреса: 0x20, 0x22 и 0xC0. При обращении по адресу 0хС0 модуль будет работать в режиме совместимости с TEA5767 фирмы Philips. Он имеет набор 8-битных регистров. Однако чип уже, наверно, лет десять как снят с производства, поэтому данный режим мы использовать не будем. Нативный режим работы подразумевает работу с 16-битными адресами. Основной управляющий регистр условно обозначенный как "CONTROL_REG" имеет адрес 0х02. Основные функции управления тюнером, такие как: включение, сброс, переключение станции и пр., осуществляются через него. При обращении по I2C адресу 0x20, внутренний счетчик регистров RDA5807m автоматически устанавливается в на этот регистр, т.е. в значение 0x02. Это удобно. При обращении по I2C адресу 0x22, регистр следует указывать.

Напомню порядок работы с I2C сессией RDA5807m при произвольном доступе:

запись произвольного регистра в RDA5897M: 1)начало сессии: формируется START 2)запись байта : посылается адрес 0x22 3)запись байта : посылается адрес записываемого регистра 4)запись байта : записывается старший байт регистра 5)запись байта : записывается младший байт регистра 6)завершение сессии: формируется STOP чтение произвольного регистра в RDA5897M: 1)начало сессии: формируется START 2)запись байта : посылается адрес 0x22 3)запись байта : посылается адрес считываемого регистра 4)завершение сессии: формируется STOP 5)начало сессии: формируется START 2)запись байта : посылается адрес (0x22 + 0х01) // режим чтения 4)чтение байта : считывается старший байт регистра 5)чтение байта : считывается младший байт регистра 6)завершение сессии: формируется STOP

Т.к. при I2C сессии сначала происходит обращение к старшему байту, а потом к младшему, то RDA5807m является чипом с big-endian порядком следования байтов. К счастью, STM8 тоже имеет прямой порядок следования байтов, и это очень удобно, т.к. мы можем сразу взять значение 16-битного регистра из буфера: ldw x,$(адрес). Другое дело, что STM8 имеет минимум операций для работы с 16-битными регистрами, и если нам нужно будет использовать логическое сложение или умножение, то старший байт и младший байт регистра придется обрабатывать отдельно друг от друга. Здесь можно было бы вспомнить, что есть такая штука, как ARM, но эта статья не об этом :)

В целом, алгоритм работы программы следующий. Имеется подпрограмма rda5807m_update, которая читает регистры [02-07] RDA_5807m. Она вызывается из главного цикла, если устанавливается переменная RDA_STAT. При первом включении RDA_STAT установлен по умолчанию, это заставляет сразу отработать rda5807m_update, после чего RDA_STAT сбрасывается. Таким образом мы всегда имеем в оперативке актуальную копию регистров [02-07] RDA_5807m, и если нужно прочитать состояние каких-либо регистров, можно "не дергать" I2C шину. Когда поступает команда через UART интерфейс, то регистры модифицируются и передаются (записываются) в RDA5807m, после этого снова устанавливается флаг RDA_STAT, и rda5807m_update из главного цикла обновляет копию содержимого регистров [02-07] RDA_5807m. Такая нехитрая логика.

Чтобы карта регистров RDA5807m была перед глазами, я скопировал ее из прошлогодней статьи и поместил под спойлер:

Теперь давайте рассмотрим код драйвера. В проекте файлы string.asm, utils.asm, uart1.asm и irq.asm остаются без изменения. На этом этапе добавляются файлы и кодом i2c.asm и rda5807.asm, а main.asm соответственно модифицируется.

Драйвер "i2c.asm" для работы с I2C модулем STM8S имеет следующий вид:

stm8/
    INTEL
    #include "STM8S103F.inc"

    segment 'rom'

.init_i2c:
    ;----------- Begin  I2C routine ------------------
    ; uint8_t init_i2c(uint8_t adr, uint8_t data);
    ;   Send Address
    bset I2C_CR2,#0         ; START
wait_start_tx:              ; wait SB in I2C_SR1
    btjf I2C_SR1, #0, wait_start_tx
    ld a,I2C_SR1            ; Clear SB bit
    ld a, (03,sp)
    ld I2C_DR,a             ; send I2C address
wait_adr_tx:
    btjt I2C_SR2,#2, fall_init_i2c ; if NACK
    btjf I2C_SR1,#1, wait_adr_tx
    ld a,I2C_SR1            ; clear ADDR bit
    ld a,I2C_SR3            ; clear ADDR bit
    ld a,(04,sp)
    ld I2C_DR,a             ; send data
wait_zero_tx:               ; wait set TXE bit
    btjf I2C_SR1,#7, wait_zero_tx
;--------------------------------------------------
    ld a, #0                ; return OK
    ret
fall_init_i2c:
    ld a,#1
    ret

    ;----------- read array from I2C --------------------------
    ;  void read_i2c(uint8_t adr, uint8_t count, uint8_t *data);
.read_i2c:
    push a
    pushw x
;--------------------------------------------------
    bset I2C_CR2,#2             ; set ACK bit
    bset I2C_CR2,#0             ; START
wait_start_rx:                  ; wait SB in I2C_SR1
    btjf I2C_SR1, #0, wait_start_rx
    ld a,I2C_SR1                ; Clear SB bit
    ld a,(06,sp)                ; a=i2c_address
    inc a                       ; read mode
    ld  I2C_DR, a               ; send i2c adr
wait_adr_rx:
    btjf I2C_SR1,#1, wait_adr_rx
    ; --------- READ BYTES --------------------
    bset I2C_CR2,#2             ; send ACK
    ld a,I2C_SR1                ; clear ADDR bit
    ld a,I2C_SR3                ; clear ADDR bit
    ld a,(08,sp)                ; begin of buffer
    clrw x
    ld xl,a
    dec (07,sp)                 ; count-1
    ; --------- READ LOOP ----------------------
wait_read:
    btjf I2C_SR1,#6,wait_read
    ld a,I2C_DR                 ; read i2c data
    ld (x),a                    ; store date to buffer
    incw x
    dec (07,sp)                 ; decrement counter
    jrne wait_read              ; if counter not equal zero then read again
    ;--  get last byte ---      ; else send NACK and read last byte
    bres I2C_CR2,#2             ; NACK
    bset I2C_CR2,#1             ; STOP
wait_last:                      ; wait RXNE bit
    btjf I2C_SR1,#6, wait_last
    ld a,I2C_DR                 ; get last byte
    ld (x), a                   ; store date to buffer
    bres I2C_CR2,#7             ; set SWRST
    ;--------------------------------------------------
    popw x
    pop a
    ret


    ;----------- write byte to I2C --------------------------
    ; void write_i2c(uint8_t value);
    ; input parameter: A - register
.write_i2c:
    ;ld a,(03,sp)
    ld I2C_DR,a             ; send data
write_i2c_loop:             ; wait set TXE bit
    btjf I2C_SR1,#7, write_i2c_loop
    ret


    end

Данные подпрограммы были скопированы из статьи STM8S + SDCC: Программирование БЕЗ SPL. Интерфейсы: UART в режиме передатчика, АЦП в режиме однократного замера, I2C в режиме мастера на примере DS1307/DS3231, там они подробно рассматривались. Функции были написаны чтобы соответствовать I2C API Arduino, дабы переписать код того или иного драйвера можно было бы с минимальными усилиями. Замечу, что по сравнению с вышеупомянутой статьей, из подпрограммы init_i2c были изъяты команды передачи на шину состояний START и STOP, а также вкл/выкл I2C модуля STM8: enable_i2c, disable_i2c.

Драйвер для работы с RDA5807m реализован в файле rda5807.asm:

stm8/
    INTEL
    #include "STM8S103F.inc"
    extern init_i2c, read_i2c, write_i2c

;-------- Constants ----------------------
RDA5807_CTRL equ 10h
RDA5807M_SEQ_I2C_ADDRESS equ 20h
RDA5807M_RND_I2C_ADDRESS equ 22h
RDA5807M_CTRL_REG equ 02h

enable_i2c  MACRO
    bset I2C_CR1,#0
    MEND

disable_i2c MACRO
    bres I2C_CR1,#0
    MEND

stop_i2c MACRO
    bset I2C_CR2,#1         ; STOP
;---------------------------------------------------
    bres I2C_CR2,#7         ; set SWRST
    MEND

    segment 'rom'

    ; ------- rda5807m_update ----------------------
    ; read six 16-bit registers [02-07] to buffer "RDA5807_CTRL" (adr 0x10-0x1c]
.rda5807m_update
    enable_i2c
    push #02                        ; select control register of rda5807m
    push #RDA5807M_RND_I2C_ADDRESS  ; =0x22
    call init_i2c
    addw sp,#02
    tnz a                           ; check return of init_i2c
    jrne rda5807m_update_quit       ; if (init_i2c != OK) then return with error
    stop_i2c                        ; else reading 12 bytes from rda5807m
    push #RDA5807_CTRL              ; buffer adr
    push #12                        ; read 12 bytes
    push #RDA5807M_RND_I2C_ADDRESS  ; =0x22 
    call read_i2c
    addw sp,#03
    clr a                           ; return success
rda5807m_update_quit:               ; quit
    disable_i2c
    ret

    ; ------- rda5807m_control_write ----------------------
    ; write to 0x02 register aka "RDA5807M_CTRL_REG"
    ; input argument: (sp+3)=high_byte, (sp+4)=low_byte
.rda5807m_control_write
    enable_i2c
    ld a,(03,sp)
    push a
    push #RDA5807M_SEQ_I2C_ADDRESS  ;=0x20
    call init_i2c
    addw sp,#02
    tnz a
    jrne rda5807m_control_write_quit
    ld a,(04,sp)
    call write_i2c
    stop_i2c
    clr a
rda5807m_control_write_quit:
    disable_i2c
    ret

    ; ------- rda5807_write_register ----------------------
    ; write 16-bit value to rda5807m register
    ; input argument: (sp+3)=register
    ; (sp+4)=high_byte, (sp+5)=low_byte
.rda5807m_write_register
    enable_i2c
    ld a,(03,sp)                    ; value
    push a
    push #RDA5807M_RND_I2C_ADDRESS  ; rda5807m I2C address
    call init_i2c
    addw sp,#02
    tnz a
    jrne rda5807m_write_register_quit
    ld a,(04,sp)
    call write_i2c                  ; write high byte of register
    ld a,(05,sp)
    call write_i2c                  ; write low byte of register
    stop_i2c
    clr a
rda5807m_write_register_quit
    disable_i2c
    ret

    end

Здесь нет ничего особенного. Подпрограмма rda5807m_update читает регистры [02-07] (12 байт) RDA_5807m и сохраняет их содержимое в массиве начиная с адреса 0х10. Подпрограмма rda5807m_control_write записывает содержимое регистра REG_02 (CONTROL). Подпрограмма rda5807m_write_register записывает содержимое произвольного регистра.

Главная программа main.asm теперь выглядит так:

stm8/
    #include "STM8S103F.inc"
    extern delay,uart1_print_str,uart1_print_num,uart1_print_char,strcmp,strnum
    extern strfrac,check_cmd, uart1_print_str_nl
    extern rda5807m_update,rda5807m_control_write, rda5807m_write_register

print_nl MACRO
    ld a, #$a
    call uart1_print_char
    MEND
print_str MACRO msg
    ldw x, #msg
    call uart1_print_str
    MEND
print_str_nl MACRO msg
    ldw x, #msg
    call uart1_print_str_nl
    MEND

LED equ 5
LEN equ 10
EOL equ LEN         ; =Zero always
;-------- Variables ----------------------
STR equ 0                   ; buffer[10bytes]
INDEX   cequ    {EOL+1} ; 1 byte
READY   cequ    {INDEX+1}   ; 1 byte
RDA_STAT cequ   {READY+1}
;-------- Constants ----------------------
RDA5807_CTRL equ $10
RDA5807M_SEQ_I2C_ADDRESS equ $20
RDA5807M_RND_I2C_ADDRESS equ $22
RDA5807M_CTRL_REG equ $02
RDA5807M_CMD_RESET equ $0002
;------------------------------------------

    segment 'rom'
.main
    ;----------- Setup Clock ----------------------
    ; Setup fHSI = 16MHz
    clr CLK_CKDIVR
    ; Enable UART and I2C,  turn off other  peripherals   
    mov CLK_PCKENR1, #0
    mov CLK_PCKENR2, #0
    bset CLK_PCKENR1, #3    ; enable UART1
    bset CLK_PCKENR1, #0    ; enable I2C
    ;----------- Setup GPIO -----------------------
    ;bset PB_DDR, #LED       ; PB_DDR|=(1<<LED)
    ;bset PB_CR1, #LED       ; PB_CR1|=(1<<LED)
    ;----------- Setup UART1 ----------------------
    ; Clear
    clr UART1_CR1
    clr UART1_CR2
    clr UART1_CR3
    clr UART1_CR4
    clr UART1_CR5
    clr UART1_GTR
    clr UART1_PSCR
    ; Setup UART1, set 115200 Baud Rate
    bset UART1_CR1, #5      ; set UARTD, UART1 disable
    ; 9600 Baud Rate
    ;mov UART1_BRR2, #0x03
    ;mov UART1_BRR1, #0x68
    ; 115200 Baud Rate
    mov UART1_BRR2, #$0b
    mov UART1_BRR1, #$08
    ; 230400 Baud Rate
    ;mov UART1_BRR2, #0x05
    ;mov UART1_BRR1, #0x04
    ; 921600 Baud Rate
    ;mov UART1_BRR2, #0x01
    ;mov UART1_BRR1, #0x01
    ; Trasmission Enable
    bset UART1_CR2, #3      ; set TEN, Transmission Enable
    bset UART1_CR2, #2      ; set REN, Receiver Enable
    bset UART1_CR2, #5      ; set RIEN, Enable Receiver Interrupt 
    ; enable UART1
    bres UART1_CR1, #5      ; clear UARTD, UART1 enable
    ;------------- I2C Setup ----------------------
    bres I2C_CR1,#0         ; PE=0, disable I2C before setup
    mov I2C_FREQR,#16       ; peripheral frequence =16MHz
    clr I2C_CCRH            ; =0
    mov I2C_CCRL,#80        ; 100kHz for I2C
    bres I2C_CCRH,#7        ; set standart mode(100кHz)
    bres I2C_OARH,#7        ; 7-bit address mode
    bset I2C_OARH,#6        ; see reference manual
    ;------------- End Setup ---------------------
    clr EOL                 ;set NULL/EOL
    ldw x,#$40
clear:
    clr (x)
    decw x
    jrne clear
    mov RDA_STAT,#1
    ; let's go...
    rim                         ; enable Interrupts
start:
    clr INDEX                   ; INDEX=0
    clr READY                   ; READY=0
mloop:
    btjt READY,#0,mute          ; if buffer not empty
    jp blink                    ; if buffer empty
mute:
    ; CHECK: if was recived "mute" command
    clrw x                      ; ard of buffer: x=0
    ldw y,#cmd_mute
    call strcmp                 ; check incoming command
    jrne unmute
    bres $10,#6                 ; clear DMUTE bit
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#0            ; update
    print_str msg_mute          ; print info message
    jp start                    ; break
unmute:
    ; CHECK: if was recived "mute" command
    clrw x
    ldw y,#cmd_unmute
    call strcmp                 ; check incoming command
    jrne set_vol
    bset $10,#6                 ; set DMUTE bit
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#0            ; update
    print_str msg_unmute        ; print info message
    jp start                    ; break
set_vol:
    push #'v'
    push #'='
    call check_cmd              ; check incoming command
    jrne vol_down               ; if Z not set, then check next command
    ldw x,#02
    call strnum                 ; get integer part
    and a,#$0f                  ; mask parameter
    ldw x,$16                   ; x=REG_5 /VOLUME/
    push a
    ld a,xl
    and a,#$f0                  ; x=(x & 0xfff0)
    or a,(1,sp)                 ; x=(x | num)
    ld xl,a
    pop a
    pushw x
    push #05                    ; select REG_5 to write
    call rda5807m_write_register; write volume to REG_5 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#0            ; update
    jp start                    ; break
vol_down:
    push #'v'
    push #'-'
    call check_cmd              ; check incoming command
    jrne vol_up                 ; next
    ld a,$17                    ; a=ram[0x17] (low byte REG_05 /VOLUME/)
    and a,#$0f                  ; mask
    jreq vol_min                ; if current volume is minimum(=0)
    ;------------
    print_str msg_volume        ; print info message  "volume="
    clrw x                      ; x=0
    dec a                       ; volume -=1
    ld xl,a                     ; x=a
    call uart1_print_num        ; print volume
    print_nl                    ; print NL
    ;----------------
    ldw x,$16                   ; x=REG_05 /VOLUME/
    decw x                      ; volume down
    pushw x
    push #05
    call rda5807m_write_register; write X to REG_05 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#0            ; update
    jp start                    ; break
vol_min:
    print_str msg_volume_min    ; print error message
    jp start
vol_up:
    push #'v'
    push #'+'
    call check_cmd              ; check incoming command
    jrne seek_down              ; next
    ld a,$17                    ; a=ram[0x17] (low byte REG_05 /VOLUME/)
    and a,#$0f                  ; mask
    cp a,#$0f                   ; if (a==15)
    jreq vol_max                ; if current volume is maximum(=15)
    ;------------
    print_str msg_volume        ; print info message "volume="
    clrw x
    inc a
    ld xl,a
    call uart1_print_num        ; print volume
    print_nl                    ; print NL
    ;----------------
    ldw x,$16                   ; x=REG_05 /VOLUME/
    incw x                      ; vlume up
    pushw x
    push #05
    call rda5807m_write_register; write X to REG_05 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#0            ; set "to update" flag
    jp start                    ; break
vol_max:
    print_str msg_volume_max    ; print error message
    jp start
seek_down:
    push #'s'
    push #'-'
    call check_cmd              ; check incoming command
    jrne seek_up                ; next 
    bres $10,#1                 ; seek-down
    bset $10,#0                 ; seek enable
    ldw x,$10                   ; X = ram[0x10]
    pushw x
    call rda5807m_control_write ; write X to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#0            ; set "to update" flag
    print_str msg_seekdown      ; print info message
    jp start                    ; break
seek_up:
    push #'s'
    push #'+'
    call check_cmd              ; check incoming command
    jrne on_cmd                 ; next
    bset $10,#1                 ; seek-up
    bset $10,#0                 ; seek enable
    ldw x,$10                   ; X = ram[0x10]
    pushw x
    call rda5807m_control_write ; write X to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#0            ; set "to update" flag
    print_str msg_seekup        ; print info message
    jp start                    ; break
on_cmd:
    push #'o'
    push #'n'
    call check_cmd              ; check incoming command
    jrne freq                   ; next
    print_str msg_on            ; print info message
    ldw x,$10
    ld a,xl
    or a,#RDA5807M_CMD_RESET    ; set RESET bit
    ld xl,a
    pushw x
    call rda5807m_control_write ; write to REG_02 /CONTROL/
    addw sp,#2
    ldw x,#$c10d                ; REG_02=0xC10D (Turn_ON + SEEK)
    pushw x
    call rda5807m_control_write ; write to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#0            ; update
    jp start                    ; break
freq:
    ; CHECK: if was recived "f=NUM.NUM" command
    push #'f'
    push #'='
    call check_cmd              ; check
    jrne help                   ; if not "f=" then goto help
    ldw x,#02
    call strnum                 ; get integer part
    subw sp,#2
    ld ($1,sp),a                ; (sp+1)=integer part
    call strfrac                ; get fractional part
    ld ($2,sp),a                ; (sp+2)=fractional part
    print_str msg_freq          ; print "freq="
    ld a,($1,sp)
    clrw x
    ld xl,a                     ; x=interger part
    call uart1_print_num        ; print x
    ld a, #'.'
    call uart1_print_char       ; print dot
    ld a,($2,sp)
    ld xl,a                     ; x=fractional part
    call uart1_print_num        ; print x
    print_nl                    ; NewLine
    addw sp,#2
    jp start                    ; break
help:
    ; CHECK: if was recived "?" command
    clrw x                      ; if "?"
    ld a,(x)
    cp a,#'?'                   ; check incoming command
    jrne help_2
    incw x
    ld a,(x)
    cp a,#0                     ; NULL 
    jrne help_2
    print_str msg_help          ; print help message
    jp start                    ; break
help_2:
    clrw x
    ldw y,#cmd_help
    call strcmp                 ; check incoming command
    jrne volume
    print_str msg_help          ; print help message
    jp start                    ; break
volume:
    ; get current Gain Control Bits(volume)
    push #'?'
    push #'v'
    call check_cmd              ; check incoming command
    jrne next                   ; if not "?v" then goto next
    print_str msg_volume        ; print "volume="
    ld a,$17                    ; load low byte of REG_5 /VOLUME/
    and a,#$0f                  ; mask
    clrw x
    ld xl,a                     ; x = a
    call uart1_print_num        ; print volume
    print_nl                    ; NewLine
next:
    ; ----- END OF CASE ---------------
    jp start

print_command:
    print_str msg_command
    clrw x
    call uart1_print_str_nl     ; print incoming string
    jp start
blink:
    btjf RDA_STAT,#0, blink_delay
    bres RDA_STAT,#0
    call rda5807m_update        ; read rda5807m
    tnz a
    jrne blink_delay
    print_str msg_update
blink_delay:
    ldw x,#50                   ; delay(50ms)
    call delay
    jp mloop
msg_error:
    STRING "incorrect value!",$0a,$00
cmd_help:
    STRING "help",$00
cmd_mute
    STRING "mute",$00
cmd_unmute
    STRING "unmute",00
msg_volume_max:
    STRING "volume is max",$0a,$00
msg_volume_min:
    STRING "volume is min",$0a,$00
msg_freq:
    STRING "freq=",$00
msg_volume:
    STRING "volume=",$00
msg_command:
    STRING "command: ",$00
msg_update:
    STRING "Read RDA5807m... ",$0a,$00
msg_on:
    STRING "Turn on",$0a,$00
msg_seekdown:
    STRING "Seek Down",$0a,$00
msg_seekup:
    STRING "Seek Up",$0a,$00
msg_mute:
    STRING "mute ON",$0a,$00
msg_unmute:
    STRING "mute OFF",$0a,$00
msg_help:
    STRING "Available commands:",$0A
    STRING "* s-/s+         - seek down/up with band wrap-around",$0A
    STRING "* v-/v+       - decrease/increase the volume",$0A
    STRING "* b-/b+       - bass on/off",$0A
    STRING "* d-/d+       - debug print on/off",$0A
    STRING "* mute/unmute - mute/unmute audio output",$0A
    STRING "* ww/jp/ws/es - cahnge band to: World Wide/Japan/West Europe/East Europe",$0A
    STRING "* 50MHz/65MHZ - cahnge low edge to 50MHz or 65MHz for East Europe band",$0A
    STRING "* c-/c+       - change space: 100kHz/200kHz/50kHz/25kHz",$0A
    STRING "* rst         - reset and turn off",$0A
    STRING "* on          - Turn On",$0A
    STRING "* ?f          - display currently tuned frequency",$0A
    STRING "* ?q          - display RSSI for current station",$0A
    STRING "* ?v          - display current volume",$0A
    STRING "* ?m          - display mode: mono or stereo",$0A
    STRING "* v=num       - set volume, where num is number from 0 to 15",$0A
    STRING "* t=num       - set SNR threshold, where num is number from 0 to 15",$0A
    STRING "* b=num       - set soft blend threshold, where num is number from 0 to 31",$0A
    STRING "* f=freq      - set frequency, e.g. f=103.8",$0A
    STRING "* ?|help      - display this list",$0A,$00,
    end

Если вкратце, то по сравнению с предыдущей частью, в коде произошли следующие изменения.

В блоке инициализации была добавлена инициализация I2C модуля:

    ;------------- I2C Setup ----------------------
    bres I2C_CR1,#0         ; PE=0, disable I2C before setup
    mov I2C_FREQR,#16       ; peripheral frequence =16MHz
    clr I2C_CCRH            ; =0
    mov I2C_CCRL,#80        ; 100kHz for I2C
    bres I2C_CCRH,#7        ; set standart mode(100кHz)
    bres I2C_OARH,#7        ; 7-bit address mode
    bset I2C_OARH,#6        ; see reference manual

Соответственно в список включенной периферии был добавлен I2C модуль

    bset CLK_PCKENR1, #0    ; enable I2C

Из кода была "выброшена" инициализация GPIO B_5 со светодиодом. Данная ножка теперь отведена под шину I2C.

В блок инициализации был также добавлен код очистки(заполнения нулями) первых 0x40 байт ОЗУ. Он не является необходимым, но помогает при отладке отследить изменения в оперативке.

    ldw x,#$40
clear:
    clr (x)
    decw x
    jrne clear
    mov RDA_STAT,#1

Последняя команда устанавливает переменную RDA_STAT. Из главного цикла был убран код переключения светодиода, вместо этого было добавлено чтение регистров RDA5807m, если переменная RDA_STAT установлена:

blink:
    btjf RDA_STAT,#0, blink_delay
    bres RDA_STAT,#0
    call rda5807m_update        ; read rda5807m
    tnz a
    jrne blink_delay
    print_str msg_update
blink_delay:
    ldw x,#50                   ; delay(50ms)
    call delay
    jp mloop

Интервал между итерациями главного цикла был сокращен до 50 мс.

Порядок работы с драйвером следующий: 1) нужно включить RDA5807m подав команду "on"; 2) после этого можно подавать любую другую команду. После включения первая передаваемая команда может не сработать, если в UART попал шум при подачи питания. Поэтому я вначале посылаю "?" чтобы проверить, что есть связь с микроконтроллером, и уже потом посылаю команды для работы с RDA5807m. Вторая команда как правило выполняется без проблем. Весь смысл в том, чтобы послать символ конца строки.

Включение RDA5807m по команде "on" производится следующим кодом:

on_cmd:
    push #'o'
    push #'n'
    call check_cmd              ; check incoming command
    jrne freq                   ; next
    print_str msg_on            ; print info message
    ldw x,$10
    ld a,xl
    or a,#RDA5807M_CMD_RESET    ; set RESET bit
    ld xl,a
    pushw x
    call rda5807m_control_write ; write to REG_02 /CONTROL/
    addw sp,#2
    ldw x,#$c10d                ; REG_02=0xC10D (Turn_ON + SEEK)
    pushw x
    call rda5807m_control_write ; write to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#0            ; to update
    jp start                    ; break

Для включения RDA5807m в регистр REG_02 (CONTROL) дважды пишутся команды: "reset" и затем "turn on+seek+seek_down". Команда "turn on" совмещена с командами "seek" и "seed down", чтобы после включения приемник сразу нашел станцию, и пошел звук. Во втором случае в REG_02 (CONTROL) записывается число 0xC10D. В двоичном виде оно выглядит как 1100 0001 0000 1101. В ассемблере нет такого мощного перепроцессора как в Си, поэтому приходится пользоваться "магическими числами", вместо того, чтобы задавать числа в виде: "ФЛАГ1 | ФЛАГ2 | ФЛАГ3" и т.д. Число 0xC10D устанавливает флаги: DHIZ, DMUTE, SEEK, RDS_EN, NEW_METHOD, ENABLE:

Флаги DHIZ и DMUTE включают аудиовыход, SEEK задает команду на поиск следующей станции, и т.к. SEEKUP сброшен в ноль, станция будет искаться в порядке убывания частоты. По умолчанию RDA5807m использует диапазон c 87,5 МГц до 108 МГц. И следовательно вначале должна найтись станция наиболее близкая к частоте 108 МГц. Флаг RDS_EN устанавливается на будущее. Чтобы установка флага NEW_METHOD как-то влияла на качество сигнала или приема я не ощутил, но утверждается, что чувствительность приемника с ним выше. Флаг ENABLE включает RDA5807m.

Если посмотреть отладчиком на дамп памяти, то можно будет заметить, что флаг SEEK не сохранятся в памяти RDA5807m. Когда станция будет найдена, флаг SEEK аппаратно сбрасывается:

Команды "s+" и "s-" выполняют поиск станции с инкрементом частоты или декрементом:

seek_down:
    push #'s'
    push #'-'
    call check_cmd              ; check incoming command
    jrne seek_up                ; next 
    bres $10,#1                 ; seek-down
    bset $10,#0                 ; seek enable
    ldw x,$10                   ; X = ram[0x10]
    pushw x
    call rda5807m_control_write ; write X to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#0            ; set "to update" flag
    print_str msg_seekdown      ; print info message
    jp start                    ; break

Они устанавливают или сбрасывают флаг "SEEK_UP" и выставляют флаг "SEEK".

Команды "mute" и "unmute" действуют похожим образом, они устанавливают или сбрасывают флаг DMUTE:

mute:
    ; CHECK: if was recived "mute" command
    clrw x                      ; ard of buffer: x=0
    ldw y,#cmd_mute
    call strcmp                 ; check incoming command
    jrne unmute
    bres $10,#6                 ; clear DMUTE bit
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#0            ; update
    print_str msg_mute          ; print info message
    jp start                    ; break

Команда "v=число" устанавливает громкость звука, где число 15 является максимальной громкостью, а ноль - минимальной. Замечу, что ноль - это не отключение звука, это минимальный уровень громкости. Уровень громкости задаётся в младших четырех битах пятого регистра RDA5807m:

set_vol:
    push #'v'
    push #'='
    call check_cmd              ; check incoming command
    jrne vol_down               ; if Z not set, then check next command
    ldw x,#02
    call strnum                 ; get integer part
    and a,#$0f                  ; mask parameter
    ldw x,$16                   ; x=REG_5 /VOLUME/
    push a
    ld a,xl
    and a,#$f0                  ; x=(x & 0xfff0)
    or a,(1,sp)                 ; x=(x | num)
    ld xl,a
    pop a
    pushw x
    push #05                    ; select REG_5 to write
    call rda5807m_write_register; write volume to REG_5 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#0            ; update
    jp start                    ; break

Команды "v+" и "v-" соответственно уменьшают или увеличивают громкость звука. Для этого они читают пятый регистр из копии в ОЗУ, выделяют из него значение текущей громкости, проверяют на допустимость диапазона, после чего производят операции сложения или вычитания единицы с последующей записью регистра в RDA5807m или выводят сообщение о недопустимости диапазона:

vol_up:
    push #'v'
    push #'+'
    call check_cmd              ; check incoming command
    jrne seek_down              ; next
    ld a,$17                    ; a=ram[0x17] (low byte REG_05 /VOLUME/)
    and a,#$0f                  ; mask
    cp a,#$0f                   ; if (a==15)
    jreq vol_max                ; if current volume is maximum(=15)
    ;------------
    print_str msg_volume        ; print info message "volume="
    clrw x
    inc a
    ld xl,a
    call uart1_print_num        ; print volume
    print_nl                    ; print NL
    ;----------------
    ldw x,$16                   ; x=REG_05 /VOLUME/
    incw x                      ; vlume up
    pushw x
    push #05
    call rda5807m_write_register; write X to REG_05 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#0            ; set "to update" flag
    jp start                    ; break
vol_max:
    print_str msg_volume_max    ; print error message
    jp start

7) Чтение частоты станции и переключение тюнера на заданную частоту

Теперь нужно реализовать печать частоты текущей станции и переключение тюнера на указанную частоту, чтобы можно было переключаться между станциями в произвольном порядке. Частоту станции при этом можно указывать вручную через UART интерфейс или брать из памяти сохраненных станций. В последнем случае, сканирование всего диапазона придется делать лишь единожды.

Начнем с печати частоты текущей станции. И здесь есть нюанс. Когда мы например вводим команды s+/s- для переключения на следующую станцию, тюнер какое-то время перенастраивает частоту, и нам нужно поймать время, когда тюнер закончит свою работу. Это может быть одна или две секунды, или этого вообще может не произойти, если вы, например, переключились на неиспользуемый диапазон. Нам нужен какой-то флаг который бы говорил нам, что работа тюнера завершена, и он настроился на заданную частоту. И такой флаг в регистрах RDA5807m есть.

Регистры RDA5807m можно разделить на два блока. Первый блок - это регистры [0х02-0х07]. Это контрольный блок, блок управления. Через них осуществляется управление FM-приемником. Второй блок - это регистры [0x0A - 0x0F]. Это что-то вроде приборной панели, это регистры по которым мы узнаем о состоянии устройства. Их мы только читаем. Из этого, второго блока, нас сейчас будет интересовать регистр 0х0А, а точнее его поля READCHAN и STC:

STC - является тем заветным флагом, который говорит нам о том, что тюнер завершил настройку на станцию. В READCHAN при этом записывается частота станции. Частота станции задается в количестве рабочих интервалов - "channel spacing", за вычетом нижней границы диапазона. По умолчанию, используемый диапазон: 87 МГц - 108МГц, а интервал 100 кГц, или 0.1МГц. Тогда частота 88 МГц будет записываться как число 10. Частота 88.1 МГц - это 11, 88.2 МГц - 12 и т.д. Диапазоны и величину интервалов можно переключать между некоторыми значениями, но по умолчанию их значения такие.

READCHAN - это 10-битное число, что подразумевает, что для вычисления частоты придется использовать 16-битную арифметику. В случае, если приемник будет использоваться только со значениями интервала и диапазона заданными по умолчанию, то достаточно будет 8-битной арифметики, т.к. максимальная частота 108 МГц будет в этом случае выражена числом 210. Но, т.к. я планирую в дальнейшем добавить возможность изменения интервалов на 50 кГц и 25 кГц, то я изначально буду использовать арифметику на 16-битных регистрах.

Ok, переходим к коду. Во-первых нам понадобится подпрограмма чтения регистров [0x0A - 0x0F] - :

    ; ------- rda5807m_rds_update ----------------------
    ; read six 16-bit registers [0a-0f] to buffer "RDA5807_RDS" (adr 0x20-0x2c]
.rda5807m_rds_update
    enable_i2c
    push #0ah                       ; select first register for read
    push #RDA5807M_RND_I2C_ADDRESS  ; =0x22
    call init_i2c
    addw sp,#02
    tnz a                           ; check return of init_i2c
    jrne rda5807m_rds_update_quit   ; if (init_i2c != OK) then return with error
    stop_i2c                        ; else reading 12 bytes from rda5807m
    push #RDA5807_RDS               ; buffer adr
    push #12                        ; read 12 bytes
    push #RDA5807M_RND_I2C_ADDRESS  ; =0x22 
    call read_i2c
    addw sp,#03
    clr a                           ; return success
rda5807m_rds_update_quit:           ; quit
    disable_i2c
    ret

Подпрограмма практически не отличается от rda5807m_update, я только поменял начальный регистр для чтения с RDA5807m (select first register for read) и адрес записи в ОЗУ микроконтроллера (buffer adr).

Порядок чтения с RDA5807m будет таким:

blink:
    btjf RDA_STAT,#0, blink_delay   ; if need read from RDA5807m...
    call rda5807m_rds_update        ; then read RDS block reg[0x0a - 0x0f]
    tnz a
    jrne blink_delay                ; if failed
    btjf RDA5807M_RDS_H,#6, blink_delay ; if Seek not Complete  

    call rda5807m_update            ; read CONTROL block of rda5807m reg[0x02 - 0x07]
    tnz a
    jrne blink_delay                ; if read was failed
    bres RDA_STAT,#0                ; if success, then 1) reset flag
    print_str msg_update            ; 2) print message: "Read RDA5807m... "

Здесь сначала с помощью подпрограммы rda5807m_rds_update читаются регистры [0x0A - 0x0F], после чего проверяется флаг STC, и если он установлен, то только после этого запускает подпрограмма rda5807m_update для чтения регистров [0x02 - 0x07].

    btjf RDA5807M_RDS_H,#6, blink_delay ; if Seek not Complete  

RDA5807M_RDS_H здесь - это адрес в ОЗУ равный 0х20, котрый задается через следующие макро-определения:

RDA5807_CTRL equ $10
RDA5807_RDS cequ {RDA5807_CTRL+$10}
RDA5807M_RDS_H  equ RDA5807_RDS

Использование памяти микроконтролера на данный момент такое. Адреса с 0х00 по 0х0С - это входящий буфер UART, плюс переменные EOL, INDEX и READY. По адресам 0х10 - 0х1B хранятся прочитанные регистры RDA5807m [0x02-0x07]. По адресам 0х20 - 0х2B хранятся прочитанные регистры RDA5807m [0x0A-0x0F].

Хочу заметить, что кроме флага STC, имеется также флаг SF, который свидетельствует об неудачной попытке настроить тюнер.

Т.к. настройка тюнера может занимать секунду или более, я поменял задержку между итерациями главного цикла с 50мс на 100мс:

blink_delay:
    ldw x,#100                      ; delay(100ms)
    call delay

Нам потребуются константы интервалов и нижних границ диапазонов для преобразования READCHAN в мегагерцы:

band_range:
    DC.W 87,76,76,65,50
spaces:
    DC.B 10,5,20,40

Здесь первые значения band_range и spaces - это значения по умолчанию. Интервалы заданы не в kHz, а в делителях одного мегагерца. Т.е. 100кГц = 1МГц/10, 200кГц = 1МГц/5, 50кГц = 1МГц/20, 25кГц = 1МГц/40.

Преобразование READCHAN в мегагерцы, и печать полученной частоты осуществляется следующим кодом:

    btjf RDA_STAT,#1,blink_delay    ; if print of station frequency and rssi
    ldw x,RDA5807M_RDS_H            ; print frequency. X= 0x0A reg
    ld a,xh
    and a,#$03                      ; mask
    ld xh,a
    clrw y
    ld a,spaces                     ; load scaler to Y reg
    ld yl,a
    divw x,y                        ; X = X / scaler
    addw x,band_range               ; X = X + low range of current band
    pushw x
    print_str msg_freq              ; print X
    popw x
    call uart1_print_num
    ld a,#'.'
    call uart1_print_char           ; print dot
    ldw x,y
    call uart1_print_num            ; print fractional part of frequency
    print_str msg_mhz

В комментариях я постарался описать стадии вычислений.

Переключить тюнер на нужную станцию можно с помощью регистра 0х03. Для этого следует выполнить обратное преобразование частоты в CHAN, сдвинуть полученное значение на 6 битов влево, затем прилюсовать флаг TUNE, чтобы запустить тюнер на настройку заданной частоты, и сохранить полученное значение в регистре 0x03.

Обратите внимания на значения по-умолчанию для интервалов (spaces) и диапазона (band). Они подчеркнуты синим.

Программа для установки частоты у меня получилась такой:

freq:
    ; CHECK: if was recived "f=NUM.NUM" command
    push #'f'
    push #'='
    call check_cmd              ; check
    jrne help                   ; if not "f=" then goto help
    ldw x,#02
    call strnum                 ; get integer part
    ld yh,a                     ; store integer part
    call strfrac                ; get fractional part
    ld yl,a                     ; store fractional part
    ld a,yh
    sub a,{band_range+1}        ; a=(integer part)(MHz) - (lower edge of band)(MHz)
    clrw x
    ld xl,a                     ; x=a
    ld a,#10
    mul x,a                     ; x=x*10 (convert MHz to hundreds of kHz)
    clr a
    ld yh,a                     ; y = (fractional part)
    pushw y
    addw x,(1,sp)               ; X=(integer part + fractional part)
    popw y
    ld a,#$40
    mul x,a                     ; (x<<6)
    ld a,$13                    ; a= low byte of REG_3 /TUNE/
    and a,#$3f                  ; mask
    push a
    ld a,xl
    or a,(1,sp)                 ; X = X | (masked low byte of REG_3 /TUNE/)
    push #$10
    or a,(1,sp)                 ; set flag "TUNE Enable"
    ld xl,a
    addw sp,#2
    pushw x
    push #03
    call rda5807m_write_register; write REG_3 /TUNE/
    addw sp,#03
    bset RDA_STAT,#0            ; to update
    bset RDA_STAT,#1            ; print freq
    jp start                    ; break

Здесь для сдвига влево на шесть битов используется умножение на 0х40. Остальные действия я расписал в комментариях.

Для некой завершенности драйвера нам нужно реализовать еще пару команд: reset и печать уровня сигнала rssi. Подача команды ресет фактически приводит к выключению FM-приемника. Она бывает полезна как альтернатива выключению питания.

Команда ресет передается установкой второго (первого, если считать с нуля) бита регистра 0х02 RDA5807m. В программе это реализуется следующим образом:

rst:
    clrw x
    ldw y,#cmd_rst
    call strcmp                 ; check incoming command
    jrne freq                   ; next
    bset $11,#1                 ; set RESET flag
    ldw x,$10                   ; load CONTROL reg to X
    pushw x
    call rda5807m_control_write ; write X to REG_02 /CONTROL/
    addw sp,#2
    jp start                    ; break

В данном случае, подавать команду на последующее чтение регистров c помощью:

 bset RDA_STAT,#0            ; to update

- не имеет смысла, т.к. RDA5807m выключится (регистры RDA5807m при этом читать можно, выключается только тюнер). Команда включения выполнит чтение в любом случае.

RSSI можно прочитать из регистра 0x0B:

Я поступил не очень красиво, и вывод RSSI я оформил не в виде подпрограммы, вместо этого я приаттачил код к выводу частоты:

print_rssi:                         ; print rssi
    ld a,$22                        ; load high byte 0x0B reg to accumulator
    srl a                           ; a = (a >> 1)
    print_str msg_rssi
    clrw x
    ld xl,a
    pushw x
    call uart1_print_num            ; print rssi
    popw x
    print_str msg_dbuv
    bres RDA_STAT,#1
    btjt RDA_STAT,#2,rssi_quit
blink_delay:
    ldw x,#100                      ; delay(100ms)
    call delay
    jp mloop
rssi_quit
    clr RDA_STAT
    jp start

Т.е. при настройке тюнера на станцию, выводится частота станции и RSSI. Тогда при запросе RSSI через команду "?q", устанавливается второй бит(если считать с нуля) флаговой переменной RDA_STAT:

rssi:
    push #'?'
    push #'q'
    call check_cmd              ; check incoming command
    jrne on_cmd                 ; next
    bset RDA_STAT,#2
    jp print_rssi               ; goto print_rssi

Всё это вводит некоторую путаницу, но это работает. Полный код файла main.asm можно посмотреть под спойлером.

stm8/
    #include "STM8S103F.inc"
    extern delay,uart1_print_str,uart1_print_num,uart1_print_char,strcmp,strnum
    extern strfrac,check_cmd, uart1_print_str_nl
    extern rda5807m_update,rda5807m_control_write, rda5807m_write_register
    extern rda5807m_rds_update

print_nl MACRO
    ld a, #$a
    call uart1_print_char
    MEND
print_str MACRO msg
    ldw x, #msg
    call uart1_print_str
    MEND
print_str_nl MACRO msg
    ldw x, #msg
    call uart1_print_str_nl
    MEND

LED equ 5
LEN equ 10
EOL equ LEN         ; =Zero always
;-------- Variables ----------------------
STR equ 0                   ; buffer[10bytes]
INDEX   cequ    {EOL+1} ; 1 byte
READY   cequ    {INDEX+1}   ; 1 byte
RDA_STAT cequ   {READY+1}
;-------- Constants ----------------------
RDA5807_CTRL equ $10
RDA5807_RDS cequ {RDA5807_CTRL+$10}
RDA5807M_SEQ_I2C_ADDRESS equ $20
RDA5807M_RND_I2C_ADDRESS equ $22
RDA5807M_CTRL_REG equ $02
RDA5807M_TUNER_REG equ $03
RDA5807M_CMD_RESET equ $0002
RDA5807M_RDS_H  equ RDA5807_RDS
;------------------------------------------

    segment 'rom'
.main
    ;----------- Setup Clock ----------------------
    ; Setup fHSI = 16MHz
    clr CLK_CKDIVR
    ; Enable UART and I2C,  turn off other  peripherals   
    mov CLK_PCKENR1, #0
    mov CLK_PCKENR2, #0
    bset CLK_PCKENR1, #3    ; enable UART1
    bset CLK_PCKENR1, #0    ; enable I2C
    ;----------- Setup GPIO -----------------------
    ;bset PB_DDR, #LED       ; PB_DDR|=(1<<LED)
    ;bset PB_CR1, #LED       ; PB_CR1|=(1<<LED)
    ;----------- Setup UART1 ----------------------
    ; Clear
    clr UART1_CR1
    clr UART1_CR2
    clr UART1_CR3
    clr UART1_CR4
    clr UART1_CR5
    clr UART1_GTR
    clr UART1_PSCR
    ; Setup UART1, set 115200 Baud Rate
    bset UART1_CR1, #5      ; set UARTD, UART1 disable
    ; 9600 Baud Rate
    ;mov UART1_BRR2, #0x03
    ;mov UART1_BRR1, #0x68
    ; 115200 Baud Rate
    mov UART1_BRR2, #$0b
    mov UART1_BRR1, #$08
    ; 230400 Baud Rate
    ;mov UART1_BRR2, #0x05
    ;mov UART1_BRR1, #0x04
    ; 921600 Baud Rate
    ;mov UART1_BRR2, #0x01
    ;mov UART1_BRR1, #0x01
    ; Trasmission Enable
    bset UART1_CR2, #3      ; set TEN, Transmission Enable
    bset UART1_CR2, #2      ; set REN, Receiver Enable
    bset UART1_CR2, #5      ; set RIEN, Enable Receiver Interrupt 
    ; enable UART1
    bres UART1_CR1, #5      ; clear UARTD, UART1 enable
    ;------------- I2C Setup ----------------------
    bres I2C_CR1,#0         ; PE=0, disable I2C before setup
    mov I2C_FREQR,#16       ; peripheral frequency =16MHz
    clr I2C_CCRH            ; =0
    mov I2C_CCRL,#80        ; 100kHz for I2C
    bres I2C_CCRH,#7        ; set standart mode(100кHz)
    bres I2C_OARH,#7        ; 7-bit address mode
    bset I2C_OARH,#6        ; see reference manual
    ;------------- End Setup ---------------------
    clr EOL                 ;set NULL/EOL
    ldw x,#$40
clear:
    clr (x)
    decw x
    jrne clear
    mov RDA_STAT,#1
    ; let's go...
    rim                         ; enable Interrupts
start:
    clr INDEX                   ; INDEX=0
    clr READY                   ; READY=0
mloop:
    btjt READY,#0,mute          ; if buffer not empty
    jp blink                    ; if buffer empty
mute:
    ; CHECK: if was recived "mute" command
    clrw x                      ; ard of buffer: x=0
    ldw y,#cmd_mute
    call strcmp                 ; check incoming command
    jrne unmute
    bres $10,#6                 ; clear DMUTE bit
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#0            ; update
    print_str msg_mute          ; print info message
    jp start                    ; break
unmute:
    ; CHECK: if was recived "mute" command
    clrw x
    ldw y,#cmd_unmute
    call strcmp                 ; check incoming command
    jrne set_vol
    bset $10,#6                 ; set DMUTE bit
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#0            ; update
    print_str msg_unmute        ; print info message
    jp start                    ; break
set_vol:
    push #'v'
    push #'='
    call check_cmd              ; check incoming command
    jrne vol_down               ; if Z not set, then check next command
    ldw x,#02
    call strnum                 ; get integer part
    and a,#$0f                  ; mask parameter
    ldw x,$16                   ; x=REG_5 /VOLUME/
    push a
    ld a,xl
    and a,#$f0                  ; x=(x & 0xfff0)
    or a,(1,sp)                 ; x=(x | num)
    ld xl,a
    pop a
    pushw x
    push #05                    ; select REG_5 to write
    call rda5807m_write_register; write volume to REG_5 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#0            ; update
    jp start                    ; break
vol_down:
    push #'v'
    push #'-'
    call check_cmd              ; check incoming command
    jrne vol_up                 ; next
    ld a,$17                    ; a=ram[0x17] (low byte REG_05 /VOLUME/)
    and a,#$0f                  ; mask
    jreq vol_min                ; if current volume is minimum(=0)
    ;------------
    print_str msg_volume        ; print info message  "volume="
    clrw x                      ; x=0
    dec a                       ; volume -=1
    ld xl,a                     ; x=a
    call uart1_print_num        ; print volume
    print_nl                    ; print NL
    ;----------------
    ldw x,$16                   ; x=REG_05 /VOLUME/
    decw x                      ; volume down
    pushw x
    push #05
    call rda5807m_write_register; write X to REG_05 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#0            ; update
    jp start                    ; break
vol_min:
    print_str msg_volume_min    ; print error message
    jp start
vol_up:
    push #'v'
    push #'+'
    call check_cmd              ; check incoming command
    jrne seek_down              ; next
    ld a,$17                    ; a=ram[0x17] (low byte REG_05 /VOLUME/)
    and a,#$0f                  ; mask
    cp a,#$0f                   ; if (a==15)
    jreq vol_max                ; if current volume is maximum(=15)
    ;------------
    print_str msg_volume        ; print info message "volume="
    clrw x
    inc a
    ld xl,a
    call uart1_print_num        ; print volume
    print_nl                    ; print NL
    ;----------------
    ldw x,$16                   ; x=REG_05 /VOLUME/
    incw x                      ; vlume up
    pushw x
    push #05
    call rda5807m_write_register; write X to REG_05 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#0            ; set "to update" flag
    jp start                    ; break
vol_max:
    print_str msg_volume_max    ; print error message
    jp start
seek_down:
    push #'s'
    push #'-'
    call check_cmd              ; check incoming command
    jrne seek_up                ; next 
    bres $10,#1                 ; seek-down
    bset $10,#0                 ; seek enable
    ldw x,$10                   ; X = ram[0x10]
    pushw x
    call rda5807m_control_write ; write X to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#0            ; set "to update" flag
    bset RDA_STAT,#1            ; print freq
    print_str msg_seekdown      ; print info message
    jp start                    ; break
seek_up:
    push #'s'
    push #'+'
    call check_cmd              ; check incoming command
    jrne rssi                   ; next
    bset $10,#1                 ; seek-up
    bset $10,#0                 ; seek enable
    ldw x,$10                   ; X = ram[0x10]
    pushw x
    call rda5807m_control_write ; write X to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#0            ; set "to update" flag
    bset RDA_STAT,#1            ; print freq
    print_str msg_seekup        ; print info message
    jp start                    ; break
rssi:
    push #'?'
    push #'q'
    call check_cmd              ; check incoming command
    jrne on_cmd                 ; next
    bset RDA_STAT,#2
    jp print_rssi               ; goto print_rssi
on_cmd:
    push #'o'
    push #'n'
    call check_cmd              ; check incoming command
    jrne rst                    ; next
    print_str msg_on            ; print info message
    ldw x,$10
    ld a,xl
    or a,#RDA5807M_CMD_RESET    ; set RESET bit
    ld xl,a
    pushw x
    call rda5807m_control_write ; write to REG_02 /CONTROL/
    addw sp,#2
    ldw x,#$c10d                ; REG_02=0xC10D (Turn_ON + SEEK)
    pushw x
    call rda5807m_control_write ; write to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#0            ; update
    bset RDA_STAT,#1            ; print freq
    jp start                    ; check incoming command
rst:
    clrw x
    ldw y,#cmd_rst
    call strcmp                 ; check incoming command
    jrne freq                   ; next
    bset $11,#1                 ; set RESET flag
    ldw x,$10                   ; load CONTROL reg to X
    pushw x
    call rda5807m_control_write ; write X to REG_02 /CONTROL/
    addw sp,#2
    jp start                    ; break
freq:
    ; CHECK: if was recived "f=NUM.NUM" command
    push #'f'
    push #'='
    call check_cmd              ; check
    jrne help                   ; if not "f=" then goto help
    ldw x,#02
    call strnum                 ; get integer part
    ld yh,a                     ; store integer part
    call strfrac                ; get fractional part
    ld yl,a                     ; store fractional part
    ld a,yh
    sub a,{band_range+1}        ; a=(integer part)(MHz) - (lower edge of band)(MHz)
    clrw x
    ld xl,a                     ; x=a
    ld a,#10
    mul x,a                     ; x=x*10 (convert MHz to hundreds of kHz)
    clr a
    ld yh,a                     ; y = (fractional part)
    pushw y
    addw x,(1,sp)               ; X=(integer part + fractional part)
    popw y
    ld a,#$40
    mul x,a                     ; (x<<6)
    ld a,$13                    ; a= low byte of REG_3 /TUNE/
    and a,#$3f                  ; mask
    push a
    ld a,xl
    or a,(1,sp)                 ; X = X | (masked low byte of REG_3 /TUNE/)
    push #$10
    or a,(1,sp)                 ; set flag "TUNE Enable"
    ld xl,a
    addw sp,#2
    pushw x
    push #03
    call rda5807m_write_register; write REG_3 /TUNE/
    addw sp,#03
    bset RDA_STAT,#0            ; to update
    bset RDA_STAT,#1            ; print freq
    jp start                    ; break
help:
    ; CHECK: if was recived "?" command
    clrw x                      ; if "?"
    ld a,(x)
    cp a,#'?'                   ; check incoming command
    jrne help_2
    incw x
    ld a,(x)
    cp a,#0                     ; NULL 
    jrne help_2
    print_str msg_help          ; print help message
    jp start                    ; break
help_2:
    clrw x
    ldw y,#cmd_help
    call strcmp                 ; check incoming command
    jrne volume
    print_str msg_help          ; print help message
    jp start                    ; break
volume:
    ; get current Gain Control Bits(volume)
    push #'?'
    push #'v'
    call check_cmd              ; check incoming command
    jrne get_freq               ; if not "?v" then goto next
    print_str msg_volume        ; print "volume="
    ld a,$17                    ; load low byte of REG_5 /VOLUME/
    and a,#$0f                  ; mask
    clrw x
    ld xl,a                     ; x = a
    call uart1_print_num        ; print volume
    print_nl                    ; print NewLine
    jp start                    ; break
get_freq:
    ; get current frequency
    push #'?'
    push #'f'
    call check_cmd              ; check incoming command
    jrne next                   ; if not "?v" then goto next
    bset RDA_STAT,#0            ; update
    bset RDA_STAT,#1            ; print freq
next:
    ; ----- END OF CASE ---------------
    jp start

print_command:
    print_str msg_command
    clrw x
    call uart1_print_str_nl         ; print incoming string
    jp start
blink:
    btjf RDA_STAT,#0, blink_delay   ; if need read from RDA5807m...
    call rda5807m_rds_update        ; then read RDS block reg[0x0a - 0x0f]
    tnz a
    jrne blink_delay                ; if failed
    btjf RDA5807M_RDS_H,#6, blink_delay ; if Seek not Complete  

    call rda5807m_update            ; read CONTROL block of rda5807m reg[0x02 - 0x07]
    tnz a
    jrne blink_delay                ; if read was failed
    bres RDA_STAT,#0                ; if success, then 1) reset flag
    print_str msg_update            ; 2) print message: "Read RDA5807m... "

    btjf RDA_STAT,#1,blink_delay    ; if print of station frequency and rssi
    ldw x,RDA5807M_RDS_H            ; print frequency. X= 0x0A reg
    ld a,xh
    and a,#$03                      ; mask
    ld xh,a
    clrw y
    ld a,spaces                     ; load scaler to Y reg
    ld yl,a
    divw x,y                        ; X = X / Scaler
    addw x,band_range               ; X = X + low range of current band
    pushw x
    print_str msg_freq              ; print X
    popw x
    call uart1_print_num
    ld a,#'.'
    call uart1_print_char           ; print dot
    ldw x,y
    call uart1_print_num            ; print fractional part of frequency
    print_str msg_mhz
print_rssi:                         ; print rssi
    ld a,$22                        ; load high byte 0x0B reg to accumulator
    srl a                           ; a = (a >> 1)
    print_str msg_rssi
    clrw x
    ld xl,a
    pushw x
    call uart1_print_num            ; print rssi
    popw x
    print_str msg_dbuv
    bres RDA_STAT,#1
    btjt RDA_STAT,#2,rssi_quit
blink_delay:
    ldw x,#100                      ; delay(100ms)
    call delay
    jp mloop
rssi_quit
    clr RDA_STAT
    jp start
band_range:
    DC.W 87,76,76,65,50
spaces:
    DC.B 10,5,20,40
msg_error:
    STRING "incorrect value!",$0a,$00
cmd_rst:
    STRING "rst",$00
cmd_help:
    STRING "help",$00
cmd_mute
    STRING "mute",$00
cmd_unmute
    STRING "unmute",00
msg_volume_max:
    STRING "volume is max",$0a,$00
msg_volume_min:
    STRING "volume is min",$0a,$00
msg_rssi:
    STRING "rssi: ",$00
msg_dbuv:
    STRING " dBuV",$0a,$00
msg_mhz:
    STRING " MHz",$0a,$00
msg_freq:
    STRING "freq=",$00
msg_volume:
    STRING "volume=",$00
msg_command:
    STRING "command: ",$00
msg_update:
    STRING "Read RDA5807m... ",$0a,$00
msg_on:
    STRING "Turn on",$0a,$00
msg_seekdown:
    STRING "Seek Down",$0a,$00
msg_seekup:
    STRING "Seek Up",$0a,$00
msg_mute:
    STRING "mute ON",$0a,$00
msg_unmute:
    STRING "mute OFF",$0a,$00
msg_help:
    STRING "Available commands:",$0A
    STRING "* s-/s+         - seek down/up with band wrap-around",$0A
    STRING "* v-/v+       - decrease/increase the volume",$0A
    STRING "* b-/b+       - bass on/off",$0A
    STRING "* d-/d+       - debug print on/off",$0A
    STRING "* mute/unmute - mute/unmute audio output",$0A
    STRING "* ww/jp/ws/es - cahnge band to: World Wide/Japan/West Europe/East Europe",$0A
    STRING "* 50MHz/65MHZ - cahnge low edge to 50MHz or 65MHz for East Europe band",$0A
    STRING "* c-/c+       - change space: 100kHz/200kHz/50kHz/25kHz",$0A
    STRING "* rst         - reset and turn off",$0A
    STRING "* on          - Turn On",$0A
    STRING "* ?f          - display currently tuned frequency",$0A
    STRING "* ?q          - display RSSI for current station",$0A
    STRING "* ?v          - display current volume",$0A
    STRING "* ?m          - display mode: mono or stereo",$0A
    STRING "* v=num       - set volume, where num is number from 0 to 15",$0A
    STRING "* t=num       - set SNR threshold, where num is number from 0 to 15",$0A
    STRING "* b=num       - set soft blend threshold, where num is number from 0 to 31",$0A
    STRING "* f=freq      - set frequency, e.g. f=103.8",$0A
    STRING "* ?|help      - display this list",$0A,$00,
    end

Полностью весь проект можно скачать с портала GitLab: https://gitlab.com/flank1er/stm8_rda5807m/tree/master/04_tune

8) Драйвер с переключением диапазонов и интервалов частот: вводная часть

RDA5807 может работать со следующими диапазонами частот:

  1. FM диапазон: 87 - 108 МГц;
  2. Японский диапазон 76 - 91 МГц;
  3. Диапазон объединяющий западный и японский: 76 - 108 МГц;
  4. Советский УКВ диапазон: 65-76 МГц;
  5. Диапазон 50-65 МГц.

Т.о. с помощью переключения диапазонов RDA5807 может принимать сигнал с частот начиная с 50 МГц по 108 МГц. В документации УКВ диапазон назван восточно-европейским, а FM диапазон - западным. В свое время частоты под диапазон УКВ выделялись между телевизионными каналами. Кроме того, советский УКВ имел полярную модуляцию, но если верить википедии, сейчас в таком формате никто не вещает "так как приёмники, способные принимать такие стереопрограммы, много лет не производятся". В прошлом году, в стране происходило отключение вещания аналогового ТВ, частоты соответственно начали освобождаться. В частности, частоту 50МГц планируют передать радиолюбителям. Как бы то ни было, сейчас во всех диапазонах, за исключением FM, стоит тишина, там никто не вещает. Отсюда возникает вопрос - для чего нам писать программу для работы с этими диапазонами?

В начале, ради сокращения текста статьи, я не хотел добавлять функционал переключения диапазонов и принимаемых интервалов к драйверу. Единственный рабочий диапазон - это FM, а станции идут с шагом в 100 кГц. Следовательно, установок по-умолчанию вполне достаточно. НО. Мне показалось, что без этого драйвер будет не полноценным. Во-вторых, если в моем городе, в эфире, за исключением FM-диапазона, стоит тишина, это не означает, что в вашем городе будет так же. В-третьих, это может быть полезно. Например, можно сделать "уоки-токи" на неиспользуемых частотах. Передатчик для УКВ диапазона собирается "на коленке".

Код переключения диапазонов и интервалов частот будет отнимать немногим более килобайта места на флеше, что на мой взгляд вполне приемлемо. Общий размер прошивки равняется 3651 байтам. Не забываем, что одним килобайтом мы можем легко пожертвовать удалив подсказку по командам "msg_help".

9) Драйвер с переключением диапазонов и интервалов частот: порядок работы с драйвером

В этот раз, в интерфейс работы с драйвером были внесены некоторые изменения, о которых я хотел бы рассказать.

Во-первых, при включении микроконтроллера должно появиться приветствие следующего вида:

RDA5807m is Ready. Enter '?' for help

При вводе знака вопроса, появляется подсказка:

Available commands: * s-/s+ - seek down/up with band wrap-around * v-/v+ - decrease/increase the volume * b-/b+ - bass on/off * d-/d+ - debug print on/off * mute/unmute - mute/unmute audio output * ww/jp/ws/es/50 - cahnge band to: World Wide/Japan/West Europe/East Europe/50MHz * c-/c+ - change space: 100kHz/200kHz/50kHz/25kHz * rst - reset and turn off * on - Turn On * ?f - display currently tuned frequency * ?q - display RSSI for current station * ?v - display current volume * ?m - display mode: mono or stereo * v=num - set volume, where num is number from 0 to 15 * t=num - set SNR threshold, where num is number from 0 to 15 * b=num - set soft blend threshold, where num is number from 0 to 31 * f=freq - set frequency, e.g. f=103.8 * ?|help - display this list

В данный момент еще остались нереализованными(уже профиксено, прим. от 23.01.20) команды: b+/b-, ?m, t=num и b=num, остальные уже работают.

При первом включении тюнер остается выключенным, если же происходил сброс питания микроконтроллера, или вы запустили отладку, то тюнер, если он был включенным, будет продолжать работать. Чтобы его выключить, следует подать команду "rst".

При выключенном тюнере мы не можем управлять состоянием RDA5807, т.к. при включении тюнера, драйвером передается команда сброса, "reset"(выделенно красным):

on_cmd:
    push #'o'
    push #'n'
    call check_cmd              ; check incoming command
    jrne rst                    ; next
    print_str msg_on            ; print info message
    ldw x,$10
    ld a,xl
    or a,#RDA5807M_CMD_RESET    ; set RESET bit
    ld xl,a
    pushw x
    call rda5807m_control_write ; write to REG_02 /CONTROL/
    addw sp,#2
    ldw x,#$c10d                ; REG_02=0xC10D (Turn_ON + SEEK)
    pushw x
    call rda5807m_control_write ; write to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#0            ; update
    bset RDA_STAT,#1            ; print freq
    jp start                    ; check incoming command

Соответственно, все изменения внесенные до передачи команды "on" будут сброшены.

Однако еще до включения тюнера можно включить отладочный принт командой "d+". В ответ мы получим подтверждение:

Debug is ON

При включенном отладочном принте, будет выводиться вспомогательная информация, которая поможет отслеживать состояние драйвера не прибегая к помощи отладчика.

Включение тюнера с отладочным принтом будет выглядеть как-то так:

Turn on Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read Control Registers Read Registers was Complete freq=107.6 MHz Space: 0 Band: 0 chan: 206 rssi: 45 dBuV

Здесь вывод сообщения "Read RDS Registers" означает чтение RDS блока регистров, а "Read Control Registers" - чтение контрольного блока.

При включении тюнера, ему сразу передается команда на поиск частоты. Это будет ближайшая к верхней границе диапазона (108 МГц) станция. В данной версии драйвера, задержка между итерациями главного цикла составляет 250 мс, и т.к. в логе появилось 12 сообщений "Read RDS Registers", следовательно, поиск станции занял 3 секунды. После этого успешно прочитался контрольный блок регистров RDA5807 и нам выдало информацию о найденной станции. Здесь chan равный 206 означает частоту: а)целая часть - 87+206/10=107; б) дробная часть - 206%10=6.

Контрольный блок регистров RDA5807 читается только после настройки тюнера на станцию. Когда происходит переключение на неиспользуемый диапазон, то тюнер доходит до начала диапазона, после чего прекращает поиск станции, и мы слышим в наушниках белый шум. Выглядит это как-то так:

Change band to exUSSR Band (65-76MHz) Seek Down Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read RDS Registers Read Control Registers Read Registers was Complete freq=65.0 MHz Space: 0 Band: 3 chan: 0 rssi: 34 dBuV

В данном случае переключение происходило на УКВ диапазон.

Включение тюнера с выключенной отладкой выглядит скромнее:

Turn on freq=107.6 MHz rssi: 43 dBuV

При смене интервалов (спейсов), драйвер пересчитывает частоту, и заново настраивается на ту же частоту что и была. При включенном отладочном выводе это выглядит так:

Change space to 200kHz Read RDS Registers Read RDS Registers Read Control Registers Read Registers was Complete freq=107.0 MHz Space: 1 Band: 0 chan: 100 rssi: 54 dBuV

Т.к. было напечатано два сообщения "Read RDS Registers", то перенастройка тюнера по времени заняла от четверти до половины секунды.

Полагаю понятно, что если мы выбираем интервал 200кГц, то тюнер не сможет настроиться на нечетную частоту, если рассматривать ее в сотнях килогерц. Произойдет округление:

Read RDS Registers Read Control Registers Read Registers was Complete freq=106.1 MHz Space: 0 Band: 0 chan: 191 rssi: 53 dBuV Change space to 200kHz Read RDS Registers Read RDS Registers Read Control Registers Read Registers was Complete freq=106.0 MHz Space: 1 Band: 0 chan: 95 rssi: 50 dBuV

Здесь я сначала подал команду "?f", а затем "c+". Когда подаете команды "с+"/"c-", следите за тем, что бы регистр клавиатуры был английским. Юникод драйвер игнорирует.

Задание настройки на частоту станции командой "f=" при интервалах в 100 и 200 кГц производится с одним знаком после точки. Например: "f=106.1", "f=95.8", "f=107.6" и т.д. Если используется частотный интервал 200 кГц, то нечетная частота будет округлена в меньшую сторону. При интервалах в 50 и 25 кГц, задание частоты происходит с двумя знаками после запятой. При используемом интервале в 50кГц частота задается с пятеркой или нулем в последней цифре: например: "f=93.50", "f=93.55", "f=93.60". Если использовать другие числа в последней цифре, то они будут округлены в меньшую сторону.

Например, вывод на команду "f=96.15":

Read RDS Registers Read RDS Registers Read Control Registers Read Registers was Complete freq=96.15 MHz Space: 2 Band: 0 chan: 183 rssi: 70 dBuV

При интервале в 25кГц, последняя цифра пять при задании частоты игнорируется. Т.е. если нужно установить частоту 96.025 МГц, то пишем "f=96.02", если 96.675МГц, то пишем "f=96.67", и т.д. Например, для установки частоты 96.125 МГц вводим команду "f=96.12" и получаем следующий вывод:

Read RDS Registers Read RDS Registers Read Control Registers Read Registers was Complete freq=96.125 MHz Space: 3 Band: 0 chan: 365 rssi: 69 dBuV

10) Драйвер с переключением диапазонов и интервалов частот: использование ОЗУ драйвером

Используемая память микроконтроллера на данном этапе выглядит так:

По сравнению с прошлым разом добавились массивы для хранения содержимого регистров RDA5807. Кроме того, были добавлены три переменные: BAND, SPACE и флаговая переменная RDA_STAT.

Переменные BAND и SPACE хранят значения номеров текущего диапазона и интервала. RDA5807 хранит их значения в регистре 0x03:

Переменная BAND в отличии от своего аналога в RDA5807 может принимать значение 5, что будет означать диапазон 50-65 МГц. При присвоении переменной BAND своего значения, проверяется 9-й бит регистра 0х07:

Присваивание значений BAND и SPACE происходит сразу после чтения контрольного блока регистров RDA5807:

;-- write current space and band ---------  
    ld a,$13                        ; get low byte REG_03 /TUNER/
    and a, #$03                     ; mask bitfield [1:0]
    ld SPACE,a                      ; store SPACE value
    ld a,$13                        ; get low byte REG_03 /TUNER/
    and a,#$0c                      ; mask bitfield [3:2]
    srl a                           ; (a>>1), right logical shift
    srl a                           ; (a>>1), right logical shift
    cp a,#3
    jrne leave_50M
    btjt $1a,#1,leave_50M
    ld a,#4
leave_50M:
    ld BAND,a                       ; store BAND value

Если программа обнаруживает, что номер текущий диапазона равен трём, и при этом сброшен 9-й бит 7-го регистра, то в переменную BAND записывается четверка.

Технически, можно было избежать инструкций проверки и переходов: cp и jrne. Вместо можно было собрать значение переменной BAND из трех битов: двух битов третьего регистра, и девятого бита седьмого регистра. Но я не уверен, что программа получилась бы короче.

RDA_STAT - это флаговая переменная, различные биты которой используются как триггеры теми или иными участками программы. Пока под флаги занято пять бит:

Здесь флаг UPDATE - указывает на необходимость чтения регистров RDA5807. PRINT - указывает на необходимость печати частоты текущей станции. RSSI флаг говорит о необходимости печати RSSI. Флаг DEBUG - говорит о том, что следует печатать отладочную информацию. Флаг FIRST отрабатывает только один раз при первом запуске микроконтроллера.

Флаг FIRST понадобился потому, что драйвер может корректно работать только после того, как прочел оба банка регистров RDA5807. Но по обычной логике программы контрольный блок читается только после того, как тюнер настроился на какую-либо станцию. Для этого проверяется STC флаг. Но если микроконтроллер только включился, а тюнер еще не включен, то микроконтроллер не сможет корректно работать, и он будет постоянно читать RDS блок регистров, ожидая, пока тюнер настроится на станцию. Флаг FIRST позволяет разорвать этот замкнутый круг. При установленном флаге FIRST позволяется прочитать контрольный блок регистров, после чего флаг FIRST сбрасывается, и более не используется.

К сожалению, я не сразу догадался использовать псевдонимы для флагов в программе. Поэтому данные именна условные, а в программе вместо них используются "магические числа".

11) Драйвер с переключением диапазонов и интервалов частот: длинные переходы и реализация оператора case на ассемблере STM8

Введение флагов DEBUG и FIRST добавило условных переходов в главный цикл программы. В STM8, условные переходы вида JRXX имеют ограничение на длину перехода в 256 байт. Программа стала разрастаться, и условные переходы перестали "влезать" в это ограничение. Соответственно, чтобы обойти это ограничение, приходится писать вместо:

blink:
    btjf RDA_STAT,#0, blink_delay   ; if need read from RDA5807m...
    call rda5807m_rds_update        ; then read RDS block reg[0x0a - 0x0f]

Что-то вроде:

blink:
    btjt RDA_STAT,#0, update_rds    ; if need read from RDA5807m, then goto update_rds
    jp blink_delay                  ; else goto blink
update_rds:
    call rda5807m_rds_update        ; read RDS block reg[0x0a - 0x0f]

Многие переходы были приведены к такому виду, что добавило в код порядочное количество меток, и ухудшило читаемость программы.

Еще одним новым элементом в программе оказалась реализация Си-оператора "case". На ассемблере STM8 он раскладывается не как вереница if-else if, а как таблица с адресами переходов на альтернативные варианты, где выбор элемента таблицы осуществляется через индекс. В качестве индекса выступают значения переменных SPACE и BAND. Т.к. адреса в таблице двухбайтные, индексы умножаются на два, затем прибавляются к адресу начала таблицы и по полученному адресу осуществляется безусловный переход. Выглядит это так:

    ld a,SPACE
    sll a
    x=a
    ldw x,(chose_space,x)
    jp (x)
chose_space:
    DC.W sp_0,sp_1,sp_2,sp_3
sp_1:
    sllw y
    jra sp_0
sp_2:
    ld a,#5
    mul y,a
    jra sp_0
sp_3:
    ld a,#25
    mul y,a
sp_0:

Здесь "x=a" - это макрос присваивающий регистру Х значение аккумулятора: "clrw x; ld xl,a"

12) Драйвер с переключением диапазонов и интервалов частот: реализация переключения диапазонов и интервалов

Теперь поговорим о том, как собственно реализуется переключение диапазонов и интервалов. Начнем с интервалов.

Переключение интервалов осуществляется командами "с+" и "с-". Для пересчетов интервалов, нам нужно взять текущее значение CHAN, умножить или разделить это значение на два или четыре. После этого следует перенастроить тюнер на новые параметры. Интервалы идут в таком порядке: 100кГц, 200кГц, 50кГц, 25кГц. Для примера, если нам требуется переключить интервал с 100кГц на 200кГц, то для этого нужно: а)взять текущее значение CHAN; б) разделить его надвое; в) записать в RDA5807 новое значение CHAN и номер интервала; г) запустить тюнер на перенастройку.

В теории все выглядит несложно. В переключением диапазонов еще проще. Перезаписываем новый диапазон в RDA5807, сбрасываем значение CHAN в ноль, и запускаем тюнер на автонастройку.

На практике нам также придется переписать код печати частоты и код команды установки частоты "f=". Приступим.

Сильной стороной ассемблера, является возможность повторно использовать уже написанный код переходя на него инструкциями jra/jp. Реализация команды "с+" содержит участок, который будет впоследствии многократно использоваться. Поэтому с нее и начнем.

space_up:
    push #'c'
    push #'+'
    call check_cmd              ; check incoming command
    jrne set_vol                ; if Z not set, then check next command
    ld a,SPACE
    inc a
    and a,#3
    push a

Вначале мы получаем текущее значение интервала, и увеличиваем его на единицу. Переключение интервалов я решил сделать "скользящим", когда после максимального значения следует минимальное. Поэтому после увеличения значения интервала, мы маскируем его операцией and a,#3.

Далее идет реализация оператора case, в котором, в зависимости от значения интервала, выполняются те или иные преобразования со значением CHAN:

;-- CASE (space)
    sll a
    x=a
    ldw x,(case_recalc,x)
    jp (x)
case_recalc:
    DC.W space_100,space_200,space_50,space_25
space_100:
    print_str msg_space100
    call rda5807m_get_readchan
    srlw x
    srlw x
    jra space_recalc
space_25:
    print_str msg_space25
    call rda5807m_get_readchan
    sllw x
    jra space_recalc
space_50:
    print_str msg_space50
    call rda5807m_get_readchan
    sllw x
    sllw x
    jra space_recalc
space_200:
    print_str msg_space200
    call rda5807m_get_readchan
    srlw x

И потом идет участок, который и будет использоваться в дальнейшем неоднократно:

space_recalc:
    shiftX6
    ld a,BAND
    cp a,#4
    jrne not_50M_band
    ld a,#3
not_50M_band:
    sll a
    sll a
    or a,(1,sp)
    pushw x
    or a,(2,sp)
    or a,#$10                   ; set TUNE flag
    ld xl,a
    addw sp,#03
    pushw x
    push #03
    call rda5807m_write_register; write REG_3 /TUNE/
    addw sp,#03
    bset RDA_STAT,#0            ; to update
    bset RDA_STAT,#1            ; print freq
    jp start                    ; break

Здесь происходит запись в третий регистр RDA5807. Для этого берутся значения SPACE, BAND и CHAN, выставляется флаг TUNE, запаковывается все это в X-регистр и отправлется на запись. В отличии от предыдущей части, где сдвиг значения CHAN влево на 6 бит осуществлялся с помощью умножения на число 64, в этот раз для этого используется макрос для последовательного сдвига:

shiftX6 MACRO
    push a
    ld a,#6
    LOCAL shift
shift:
    sllw x
    dec a
    jrne shift
    pop a
    MEND

Использование умножения было возможно только с 8-битным значением CHAN, т.е. при значении интервалов в 100 или 200 кГц.

Конечно, в макросе можно было бы написать шесть друг за другом идущих инструкций "sllw x", но это было бы скучно ;)

Обработчик команды "c-" выглядит как урезанная версия рассмотренного варианта, алгоритм тот же, в завершение идет переход по метке "space_recalc":

space_down:
    push #'c'
    push #'-'
    call check_cmd              ; check incoming command
    jrne space_up               ; if Z not set, then check next command
    ld a,SPACE
    dec a
    and a,#3
    push a
;-- CASE (space)
    sll a
    x=a
    ldw x,(case_recalc2,x)
    jp (x)
case_recalc2:
    DC.W space2_100,space2_200,space2_50,space2_25
space2_100:
    print_str msg_space100
    call rda5807m_get_readchan
    sllw x
    jra space_recalc
space2_200:
    print_str msg_space200
    call rda5807m_get_readchan
    srlw x
    srlw x
    jra space_recalc
space2_50:
    print_str msg_space50
    call rda5807m_get_readchan
    srlw x
    jra space_recalc
space2_25:
    print_str msg_space25
    call rda5807m_get_readchan
    sllw x
    sllw x
    jra space_recalc

Переключение диапазона происходит проще. За пример можно взять переключение на диапазон 76-108 МГц:

band_ww:
    push #'w'
    push #'w'
    call check_cmd
    jrne debug_on
    print_str msg_change_band
    print_str msg_band_ww
    ld a,#2
set_band:
    sll a
    sll a
    or a,SPACE
    or a,#$10                   ; set TUNE flag
    x=a
    pushw x
    push #03
    call rda5807m_write_register; write REG_3 /TUNE/
    addw sp,#03
    ldw x,#200                      ; delay(200ms)
    call delay
    jp seek_down_directly

Здесь также все построено на записи в третий регистр RDA5807. При этом значение CHAN обнуляется, в регистр "скидываются" значения SPACE и BAND, устанавливается флаг TUNE и производится запись. После этого идет задержка на 200 мс для того чтобы тюнер успел настроиться на частоту, и затем идет безусловный переход на выполнение команды "s-". Т.е. как бы имитируется работа команды "on", только с другим диапазоном.

Переключение на диапазоны 76-91 и 87-108 МГц сводится к безусловному переходу по метке "set_band". Меняется только значение диапазона:

band_ws:
    push #'w'
    push #'s'
    call check_cmd
    jrne band_50M
    print_str msg_change_band
    print_str msg_band_eu
    clr a
    jp set_band

band_jp:
    push #'j'
    push #'p'
    call check_cmd
    jrne band_ww
    print_str msg_change_band
    print_str msg_band_jp
    ld a,#01
    jra set_band

В случае с переходом на УКВ диапазон ситуация осложняется тем, что приходится смотреть, что записано в 9-м бите 7-го регистра RDA5807, и также при необходимости приходится его переписывать. После этого, также идет безусловный переход на туже метку "set_band":

band_ukv:
    push #'e'
    push #'s'
    call check_cmd
    jrne band_jp
    print_str msg_change_band
    print_str msg_band_ukv
    btjt $1a,#1,omit_50M_65M
    bset $1a,#1
    ldw x,$1a
    pushw x
    push #7
    call rda5807m_write_register; write REG_7
    addw sp,#03
omit_50M_65M:
    ld a,#3
    jra set_band

С переходом на диапазон 50-65 МГц все тоже самое, отличия лишь в паре инструкций:

band_50M:
    push #'5'
    push #'0'
    call check_cmd
    jrne band_ukv
    print_str msg_change_band
    print_str msg_band_50M
    btjf $1a,#1,omit_65M_50M
    bres $1a,#1
    ldw x,$1a
    pushw x
    push #7
    call rda5807m_write_register; write REG_7
    addw sp,#03
omit_65M_50M:
    ld a,#3
    jra set_band

Обработчик команды "f=" имеет тот же алгоритм, что и в предыдущей версии, но по реализации будет несколько отличаться. Давайте рассмотрим эти отличия:

freq:
    ; CHECK: if was recived "f=NUM.NUM" command
    push #'f'
    push #'='
    call check_cmd              ; check
    jrne help                   ; if not "f=" then goto help
    ldw x,#02
    call strnum                 ; get integer part
    y=a                         ; y = integer part
    call strfrac                ; get fractional part
    push a                      ; fractional part to stack

Здесь целая часть частоты записывается в регистр Y, а дробная часть помещается в стек. Из-за того, что подпрограмма strfrac возвращает только однобайтное значение дробной части, пришлось пойти на хитрость при указании частоты для интервала в 25 кГц, когда частоту следовало бы записывать с тремя знаками после запятой. к этому мы еще вернемся.

Далее в Х регистр загружается нижняя граница текущего диапазона:

    ld a,BAND
    sll a
    x=a
    ldw  x,(band_range,x)       ; x=low edge of current band

И эта граница вычитается их целой части частоты которая продолжает находиться в регистре Y:

    pushw x
    subw y,(1,sp)               ; y = (integer part - low edge of current band)

Полученную разницу умножаем на множитель текущего интервала:

    ld a,SPACE
    x=a
    ld a,(spaces,x)             ; load scaler
    mul y,a                     ; y = y * scaler
  

Итак, целую часть мы перевели в интервалы. Теперь следует заняться дробной частью.

Перед этим загружаем из стека в регистр X дробную часть частоты, а содержимое регистра Y напротив, сохраняем в стеке:

    ld a,(3,sp)
    x=a                         ; x = fractional part
    pushw y                     ; save y

Далее у нас опять идет case-оператор:

    ld a,SPACE
    sll a
    y=a
    ldw y,(case_freq,y)
    jp (y)
case_freq:
    DC.W sp_is_0, sp_is_1, sp_is_2, sp_is_3

В этом операторе нам нужно решить как преобразовать дробную часть в интервалы. В случае, если интервал равен 100 кГц, то делать вообще ничего не надо. Дробная часть и так будет выражена в сотнях килогерц. Потом (метка sp_is_0), мы просто сложим ее с целой частью.

В случае, если интервал равен 200 кГц, то дробную часть следует разделить на два, после чего опять же прибавить к целой части(т.е. выполнить безусловный переход на sp_is_0):

sp_is_1:
    srlw x
sp_is_0:

В данном случае, и перехода делать не надо.

В случае, если если интервал равен 50 или 25 кГц, то мы вынуждены ввести формат двухцифренной записи дробной части. И если с частотой "106.15" все понятно, то с частотой "106.10" не все так однозначно. Я поясню. Моей задачей здесь было не упрощение командного интерфейса UART, а возможность будущей работы драйвера с энкодером. По большей части, планируется вводить частоту крутя ручку настройки, а UART-интерфейс использовать только для отладки.

Итак, если у нас интервал будет задан в 50 кГц, то дробная часть будет вводиться в числах вида: 5, 10, 15, 20, 25 и т.д. Тогда делением на пять, мы переведем эти значения в интервалы:

sp_is_2:
    ld a,#5
    div x,a                     ; x=x/5
    jra sp_is_0

C интервалом в 25 кГц несколько все запутано. Не уверен, у меня хватит способностей внятно это объяснить, но я попробую. Интервал 25кГц в два раза меньше чем 50 кГц, но мы не можем уменьшить делитель с пятерки до 2.5, т.к. для этого пришлось бы использовать арифметику с плавающей запятой. Поэтому, вместо этого, мы будем умножать значение дробной части на два и затем делить на пять. Здесь должно быть все ясно.

Проблема заключается в том, что вынуждены отбрасывать пятерку в третьем знаке после запятой, и при целочисленном делении мы получим искаженный результат. Чтобы этого не происходило, в качестве компенсации, можно прибавлять единицу к результату умножения.

Несколько примеров (используется целочисленное деление): (2*2+1)/5=1; (5*2+1)/2=2; (7*2+1)/5=3; (10*2+1)/5=5; (22*2+1)/5=9; (30*2+1)/5=12; (52*2+1)/5=21; (55*2+1)/5=22; (90*2+1)/5=36; (97*2+1)/5=39.

sp_is_3
    sllw x
    incw x
    ld a,#5
    div x,a
    jra sp_is_0

В качестве альтернативы, можно было бы проверять остаток от деления, но тогда получился бы алгоритм с ветвлением, что сказалось бы на быстродействии (не забываем про конвейер).

На этом все сложности заканчиваются. Теперь складываем дробную часть с целой, выравниваем указатель стека и записываем третий регистр RDA5807:

sp_is_0:
    addw x,(1,sp)               ; x=x+y
    addw sp,#5
    push SPACE
    jp space_recalc

Последний участок кода, который осталось разобрать, это печать текущей частоты. Версия с различающимися диапазонами и интервалами. Алгоритм тот же, что и в предыдущем варианте.

В регистр X записываем текущее значение CHAN:

print_freq:
    call rda5807m_get_readchan

Загружаем делитель в регистр Y:

    ld a,SPACE
    y=a
    ld a,(spaces,y)                 ; load current scaler
    y=a                             ; load scaler to Y reg

Делим CHAN на полученный делитель и целую часть частоты сохраняем в стеке:

    divw x,y                        ; X = X / Scaler
    pushw x

Далее, с помощью case оператора, остаток от деления приводим к виду: остаток * 100кГц:

    ld a,SPACE
    sll a
    x=a
    ldw x,(chose_space,x)
    jp (x)
chose_space:
    DC.W sp_0,sp_1,sp_2,sp_3
sp_1:
    sllw y
    jra sp_0
sp_2:
    ld a,#5
    mul y,a
    jra sp_0
sp_3:
    ld a,#25
    mul y,a

Затем получаем нижнию границу диапазона, и складываем ее с целой частью сохраненной в стеке:

sp_0:
    print_str msg_freq              ; print X
    ld a,BAND
    sll a
    x=a
    ldw x,(band_range,x)
    addw x,(1,sp)
    addw sp,#2

После этого производится печать вычисленных значений:

   call uart1_print_num
    ld a,#'.'
    call uart1_print_char           ; print dot
    ldw x,y
    ld a,#3
    cp a,SPACE
    jrne print_fract
    cpw x,#100
    jrsge print_fract
    ld a,#'0'
    call uart1_print_char
print_fract:
    call uart1_print_num            ; print fractional part of frequency
    print_str msg_mhz
 

13) Функции шумоподавления

RDA5807 содержит две функции шумоподавления, которые мне показались полезными. Первая - это "Threshold", которая задает порог чувствительности при автонастройке, т.е. последовательном сканировании станций в текущем частотном диапазоне. Threshold устанавливается через пятый регистр RDA5807. Его значение по умолчанию равно восьми, максимальное значение равняется пятнадцати. Данный параметр определяет, будет ли ваш приемник находить станции со слабым уровнем сигнала, или игнорировать их. Нужно отдавать себе отчет в том, что станции со слабым уровнем сигнала как правило "шуршат".

Функция "Soft Blend" устанавливает уровень шумоподавления. Если у станции хороший сигнал, то установка значения SoftBlend равному нулю сделает звук "чище". Если же станция "шуршит", то установка высокого значения SoftBlend очистит звук от "шуршания". Но при этом следует быть готовым к тому, что звук самой станции будет как из закрытого чемодана, т.е. глухой, без высоких частот. По умолчанию SoftBlend установлен в значение 16, максимальное значение параметра не может превышать цифру 31. Параметр устанавливается через седьмой регистр RDA5807:

Установка параметра Threshold производится командой "t=число", в программе ее обработчик выглядит так:

set_threshold:
    push #'t'
    push #'='
    call check_cmd              ; check incoming command
    jrne set_soft_blend         ; if Z not set, then check next command
    print_str msg_threshold
    ld a,$16                    ; load high byte REG_5 to accumulator
    and a,#$f0                  ; clear bitfield [11:8]
    ld $16,a                    ; return value from accumulator
    ldw x,#02
    call strnum                 ; get input value
    and a,#$0f                  ; set mask for bitfield [3:0]
    x=a
    call uart1_print_num
    or a,$16                    ; add with high byte REG_5
    ld $16,a
    ldw x,$16                   ; 
    pushw x
    push #5
    call rda5807m_write_register; write REG_5
    addw sp,#03
    bset RDA_STAT,#0            ; to update
    print_nl
    jp start                    ; break

Установка параметра SoftBlend производится командой "b=число", ее обработчик не сильно отличается от предыдущего примера:

set_soft_blend:
    push #'b'
    push #'='
    call check_cmd              ; check incoming command
    jrne band_ws                ; if Z not set, then check next command
    print_str msg_soft_blend
    ld a,$1a                    ; load high byte REG_7 to accumulator
    and a,#$83                  ; clear bitfield [14:10]
    ld $1a,a                    ; return value from accumulator
    ldw x,#02
    call strnum                 ; get input value
    and a,#$1f                  ; set mask for bitfield [4:0]
    x=a
    call uart1_print_num
    sll a                       ; left  shift to two bits
    sll a
    or a,$1a                    ; add with high byte REG_7
    ld $1a,a
    ldw x,$1a
    pushw x
    push #7
    call rda5807m_write_register; write REG_7
    addw sp,#03
    bset RDA_STAT,#0            ; update
    print_nl
    jp start                    ; break

Еще интересной функцией RDA5807 явлется усиление басов, которая активируется через установку 12-го бита второго регистра RDA5807:

Эффект на качество звучания будет зависеть от вашей акустики, в плеерных наушниках вы можете вообще не заметить каких-либо изменений. Мне лично такой звук не понравился, но функция в целом любопытная, хотя бы для экспериментов.

В программе, включение/выключение данной функции реализуется следующим образом:

bass_on:
    push #'b'
    push #'+'
    call check_cmd              ; check incoming command
    jrne bass_off               ; next
    bset $10,#4                 ; set BASS flag
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#0            ; update
    print_str msg_bass_on       ; print info message
    jp start                    ; break
bass_off:
    push #'b'
    push #'-'
    call check_cmd              ; check incoming command
    jrne mono                   ; next
    bres $10,#4                 ; reset BASS flag
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#0            ; update
    print_str msg_bass_off      ; print info message
    jp start                    ; break

Соответственно через UART-интерфейс функция переключается командами "b+" и "b-".

Последней функций которую мы рассмотрим перед RDS, является индикатор стерео вещания. Не берусь сказать, насколько это полезно, в FM-диапазоне на настоящее время все станции вещают в стерео. Даже информационные станции ставят время от времени музыку, и следовательно они должны вещать в стерео. За индикацию стерео сигнала отвечает 10-й бит регистра 0х0A, в программе его значение "добывается" с помощью подпрограммы:

print_stereo:
    btjt $20,#2,stereo          ; check ST flag of 0x0a register
    print_str msg_mono          ; print info message
    ret
stereo:
    print_str msg_stereo        ; print info message
    ret

Соответственно команда "?m" содержит вызов данной подпрограммы:

mono:
    push #'?'
    push #'m'
    call check_cmd              ; check incoming command
    jrne set_threshold          ; next
    call print_stereo
    jp start                    ; break

Статус стерео также автоматически печатается при выводе информации о станции в отладочном режиме:

Read RDS Registers Read Control Registers Read Registers was Complete freq=92.0 MHz Space: 0 Band: 0 chan: 50 Stereo Mode rssi: 73 dBuV

На этом данная глава заканчивается. В следующий раз мы будем разбираться с чтением RDS сообщений с помощью RDS блока регистров RDA5807m.

14) Работа над ошибками (борьба с аппаратным багом программными средствами)

Прежде чем приступать к описанию RDS, я хотел бы упомянуть об одном баге, с которым мне пришлось столкнуться при написании драйвера. Он проявился почти сразу, но описать его я решил только сейчас, т.к. долго не мог понять откуда он берется и как с ним бороться. В первой версии драйвера, который я писал на SDCC, такого не было, и поначалу я считал, что это программный баг. Однако оказалось, что не все так просто. Баг проявляется только при питании микроконтроллера от ST-Link, и это может значительно затруднить отладку программы. Поэтому я заложил в драйвер некоторые средства борьбы с ним. Эти средства не мешают работе драйвера, но если ничего не знать о баге, то эти участки кода могут показаться странными.

Баг проявляет себя как спонтанная перезагрузка микроконтроллера, вследствие чего может произойти зависание RDA5807. При старте программа начинает опрашивать RDA5807 по I2C шине и если очередная перезагрузка микроконтроллера происходит во время чтения RDA5807, то RDA5807 "зависает", т.е. становится неуправляем. звук при этом из приемника продолжает идти. Фактически, устройство находится в режиме слейва I2C и ждет, когда мастер заберет данные. ST-Link при этом внешне работает стабильно, в операционной системе нет характерных логов вида: "устройство отключилось и тут же подключилось".

Если вместо ST-Link использовать независимое питание, например, от USB-зарядки, то баг бесследно исчезает.

Перезагрузки начинают происходить особенно часто при подключении наушников к RDA5807. Отсюда у меня появилось две версии происхождения бага. Первая версия заключалась в проблемах с "землей", т.е. когда ты соединяешь землю разных устройств, которые по факту никуда не заземлены. Разность потенциалов приводит к Reset. Вторая версия заключалась в дребезге контактов, т.к. вся схема у меня собрана непаячной макетке. Сейчас я скорее склоняюсь ко второй версии, т.к. после пересборки схемы, баг куда-то пропадает.

И можно было бы вообще об этом не упоминать, но как я говорил, я внес некоторые изменения в код ради борьбы с багом, а также предпринял попытку решить все аппаратными средствами. Но обо всем по порядку.

В самом начале я говорил, что RDA5807 допускает прямое подключение плеерных 32 Ом-ных наушников. Однако, если посмотреть на даташит, то между чипом и наушниками стоит фильтр из конденсатора и дросселя. Если не ошибаюсь, это последовательный полосовой резонансный LC фильтр:

Мне показалось, что номиналы конденсаторов фильтра чрезмерно большие - 125мкФ. Я пробовал поставить следующие конденсаторы между RDA5807 и наушниками:

Здесь керамические конденсаторы (красные) на 68нФ, пленочные на 12нФ (желтые) и металло-пленочные на 4.7мкФ (синие). С конденсаторами на 12нФ звука почти не слышно, с конденсаторами на 68нФ звук тихий, конденсаторы на 4.7мкФ передают звук практически без потерь громкости. Конденсаторы проблему с багом не решили, но возможно кому-то эти цифры помогут разобраться, т.к. в сети много схем подключения наушников в RDA5807 именно через конденсаторы.

Т.к. идея с конденсаторами провалилась, в ход пошли привычные "костыли".

Во-первых, я поставил задержку на полсекунды в начале выполнения программы, чтобы защитить I2C шину, если микроконтроллер уходит в bootloop:

  72     ;-------------------------------------------
  73     ; for prevent power bounce
  74     ldw x,#500                      ; delay(500ms)
  75     call delay

Версия с программной ошибкой тоже имела некоторое основание под собой. Четвертая версия драйвера имела алгоритмическую ошибку. Работа драйвера базируется на хранении копии содержимого регистров RDA5807, но при первом запуске они еще не прочитаны. Это приводило к бесконечному опросу I2C шины, пока по UART не поступит команда "on". Самые жесткие глюки были именно с этой версией драйвера. Что бы исправить эту ситуацию, в пятой версии драйвера появился флаг First в RDA_STAT:

В третьих, при первом запуске программы я вставил вывод сообщения: "RDA5807m is Ready. Enter '?' for help"

 764 first_loop_end:
 765     bres RDA_STAT,#4
 766     print_str msg_ready

Собственно по этому сообщению я и понял, что баг заключается в произвольной перезагрузке микроконтроллера. В принципе, это признак ошибки операций со стеком, но таких ошибок я не нашел. Хотя, это сообщение мне помогало в дальнейшем выявлять этот тип ошибок при отладке драйвера.

Также я установил режим отладки по умолчанию, потому-что в случае bootloop'a вы просто не увидите никаких сообщений:

 731 blink:
 732     bset RDA_STAT,#3                ; uncomment for DEBUG

Чтобы избежать зависаний микроконтроллера при попытке достучаться до RDA5807 по I2C шине, я включил Watchdog:

 131     ; let's go...
 132     mov IWDG_KR, #$cc;
 133     mov IWDG_KR, #$55;          ; unlock IWDG_PR & IWDG_RLR
 134     mov IWDG_PR, #6             ; =256
 135     mov IWDG_RLR,#$ff           ; ~1sec for reset
 136     mov IWDG_KR, #$aa           ; lock IWDG_PR & IWDG_RLR
 137     rim                         ; enable Interrupts

Соответственно, в главном цикле производится сброс таймера Watchdog'а:

 877 delay_loop:
 878     mov IWDG_KR, #$aa               ; reset watchdog

И последнее, я доработал обработчик команды "rst":

 609 rst:
 610     clrw x
 611     ldw y,#cmd_rst
 612     call strcmp                 ; check incoming command
 613     jrne freq                   ; next
 614     bset $11,#1                 ; set RESET flag
 615     ldw x,$10                   ; load CONTROL reg to X
 616     pushw x
 617     call rda5807m_control_write ; write X to REG_02 /CONTROL/
 618     addw sp,#2
 619 
 620     ;print_str msg_reset            ; print message "Reset"
 621     ;mov RDA_STAT,#$10
 622     mov IWDG_KR, #$cc;
 623     mov IWDG_KR, #$55;          ; unlock IWDG_PR & IWDG_RLR
 624     mov IWDG_PR, #6             ; =256
 625     mov IWDG_RLR, #1
 626     mov IWDG_KR, #$aa           ; lock IWDG_PR & IWDG_RLR
 627 rst_loop:
 628     jra rst_loop

Теперь команда "rst" после выключения RDA5807, с помощью Watchdog'а перезагружает сам микроконтроллер.

Возможно баг проявляется только у меня, но все же я подумал, что будет лучше рассказать о нем.

15) Что такое RDS и как его читать с помощью RDA5807m

RDS - это возможность получать цифровые данные через обычный аналоговый FM-приемник. Если верить вики, то система была разработана для своевременного информирования водителей о дорожной обстановке. На текущий момент RDS на мой взгляд устарела, теперь все это делается через мобильный интернет (FM-радио тоже кстати устарело). Но. FM-приемник не потребляет трафик, не требует паспорта для своей работы, он дешевле и меньше потребляет энергии, нежели более функциональные беспроводные устройства. Т.е. своя ниша у FM вещания и, соответственно, RDS - есть.

Через RDS можно устанавливать время. Это на мой взгляд самая полезная функция. RDS - это самый простой и дешевый способ обеспечить автоматическую синхронизацию RTC вашего устройства. Т.е. это простое устройство которое избавит ваш прибор от лишних кнопок и клавиатур, а вас самих от нудной процедуры подводки часов, на множестве устройств. Но здесь не все так просто. Во первых, в вашем городе должна найтись FM-станция которая передает время через RDS, у нее должен быть хороший прием, и она должна передавать корректное время. В моем городе, например, нашлось только одна такая станция.

Кроме текущего времени, через RDS можно принимать радиотекст, но как правило это лишь название станции латиницей, и телефоны рекламной службы. Т.е. польза от этой информации, я бы сказал сомнительная.

В сети можно найти много информации о том как происходит модуляция и демодуляция RDS сигнала, как следует его декодировать и т.д. Нас все это не будет интересовать, т.к. это все осуществляется маленьким чипом RDA5807, а мы можем работать только с готовыми данными, которые он выдает. Единственное, что нам понадобится из документации - это описание стандарта RDS - "EN50067. Specification of the radio data system (RDS) for VHF/FM sound broadcasting in the frequency range from 87,5 to 108,0 MHz. April 1998.

В этом стандарте нас в первую очередь будет интересовать формат пакета данных:

Каждый пакет состоит из четырех блоков, а каждый блок состоит в свою очередь из двух информационных байт и контрольной суммы. Информационные байты каждого блока, это соответственно регистры RDS группы A, B, C и D:

К контрольным суммам мы доступа не имеем, у нас есть только в регистре 0x0B уровень шума для блоков A и B:

По этим четырем битам мы можем оценивать то, приняли ли мы какую-то передачу или имеем дело с шумом, т.к. некоторые станции могут вообще ничего не вещать в формате RDS.

Хочу обратить внимание, что время передачи пакета RDS составляет 87мс. Соответственно, если мы не хотим терять пакеты, (например, радиодекст может состоять из 16 последовательных пакетов) нам нужно принимать их с периодичностью меньшой этой цифры. Сейчас в драйвере чтение RDS регистров происходит с периодом равному половине этого времени, т.е. 43мс. Это дает хороший результат для уверенного чтения RDS.

Первые два блока RDS пакета можно назвать заголовком пакета. Нас будут интересовать старшие пять бит второго блока. Это т.н. Group type code. Этот код указывает на содержимое пакета. Т.е. является ли информация в пакете временем, радиотекстом, либо чем-то еще.

Коды 0A и 0B - это Service Name, через который передается название станции латиницей в кодировке ASCII. Дорожное радио например выглядит так: "DOPO*НОЕ". Емкость строки - восемь символов. Пустые символы забиваются пробелами, код 0х20. Очень часто по циклу крутят какую-то строку(не бегущую), навроде этого:

15:33:11.384 -> RADIO 15:33:11.759 -> RADIO 15:33:12.087 -> RADIO 15:33:12.414 -> RADIO 15:33:12.837 -> RADIO 15:33:13.164 -> RADIO 15:33:13.493 -> 104.5FM 15:33:13.820 -> 104.5FM 15:33:14.242 -> 104.5FM 15:33:14.570 -> 104.5FM 15:33:14.898 -> 104.5FM 15:33:15.272 -> 104.5FM 15:33:15.601 -> 104.5FM 15:33:15.928 -> 104.5FM 15:33:16.256 -> 104.5FM 15:33:16.678 -> 104.5FM 15:33:17.006 -> 104.5FM 15:33:17.333 -> ADAM 15:33:17.709 -> ADAM 15:33:18.036 -> ADAM 15:33:18.411 -> ADAM 15:33:18.739 -> ADAM 15:33:19.114 -> ADAM 15:33:19.442 -> ADAM 15:33:19.771 -> ADAM 15:33:20.192 -> ADAM 15:33:20.521 -> ADAM 15:33:20.848 -> RADIO 15:33:21.176 -> RADIO 15:33:21.598 -> RADIO 15:33:21.926 -> RADIO 15:33:22.253 -> RADIO 15:33:22.629 -> RADIO

Если использовать эту информацию для задания названия станции при автопоиске, то я например не понимаю, какую именно строку считать за название станции. Европа Плюс, например, передает такое:

17:06:04.342 -> ******** 17:06:04.717 -> EUROPA 17:06:05.045 -> EUROPA 17:06:05.372 -> EUROPA 17:06:05.795 -> EUROPA 17:06:06.122 -> EUROPA 17:06:06.450 -> EUROPA 17:06:06.778 -> EUROPA 17:06:07.153 -> EUROPA 17:06:07.480 -> EUROPA 17:06:07.809 -> PLUS 17:06:08.230 -> PLUS 17:06:08.559 -> PLUS 17:06:08.886 -> PLUS 17:06:09.214 -> PLUS 17:06:09.589 -> PLUS 17:06:09.964 -> PLUS 17:06:10.291 -> PLUS 17:06:10.667 -> PLUS 17:06:10.994 -> IZHEVSK 17:06:11.369 -> IZHEVSK 17:06:11.697 -> IZHEVSK 17:06:12.072 -> IZHEVSK 17:06:12.399 -> IZHEVSK 17:06:12.728 -> IZHEVSK 17:06:13.149 -> IZHEVSK 17:06:13.477 -> IZHEVSK 17:06:13.805 -> IZHEVSK 17:06:14.133 -> 103.0 FM 17:06:14.507 -> 103.0 FM 17:06:14.883 -> 103.0 FM 17:06:15.210 -> 103.0 FM 17:06:15.586 -> 103.0 FM 17:06:15.913 -> 103.0 FM 17:06:16.241 -> 103.0 FM 17:06:16.571 -> 103.0 FM 17:06:16.992 -> 103.0 FM 17:06:17.319 -> REKLAMA 17:06:17.648 -> REKLAMA 17:06:18.022 -> REKLAMA 17:06:18.350 -> REKLAMA 17:06:18.679 -> REKLAMA 17:06:19.053 -> REKLAMA 17:06:19.427 -> 775-779 17:06:19.756 -> 775-779 17:06:20.084 -> 775-779 17:06:20.505 -> 775-779 17:06:20.833 -> 775-779 17:06:21.161 -> 775-779 17:06:21.489 -> EUROPA 17:06:21.864 -> EUROPA 17:06:22.238 -> EUROPA 17:06:22.567 -> EUROPA 17:06:22.941 -> EUROPA 17:06:23.269 -> EUROPA 17:06:23.598 -> PLUS 17:06:23.925 -> PLUS 17:06:24.346 -> PLUS 17:06:24.675 -> PLUS 17:06:25.003 -> PLUS 17:06:25.377 -> PLUS 17:06:25.706 -> NOMER 1 17:06:26.080 -> NOMER 1 17:06:26.408 -> NOMER 1 17:06:26.783 -> NOMER 1 17:06:27.111 -> NOMER 1 17:06:27.438 -> NOMER 1 17:06:27.860 -> V ROSSII 17:06:28.188 -> V ROSSII 17:06:28.516 -> V ROSSII 17:06:28.844 -> V ROSSII 17:06:29.219 -> V ROSSII 17:06:29.594 -> V ROSSII 17:06:29.922 -> ******** 17:06:30.296 -> ******** 17:06:30.625 -> ******** 17:06:30.952 -> ******** 17:06:31.280 -> ******** 17:06:31.703 -> ******** 17:06:32.031 -> BOLSHE 17:06:32.358 -> BOLSHE 17:06:32.734 -> BOLSHE 17:06:33.061 -> BOLSHE 17:06:33.436 -> BOLSHE 17:06:33.764 -> BOLSHE 17:06:34.139 -> HITOV 17:06:34.466 -> HITOV 17:06:34.795 -> HITOV 17:06:35.216 -> HITOV 17:06:35.545 -> HITOV 17:06:35.872 -> HITOV 17:06:36.200 -> BOLSHE 17:06:36.575 -> BOLSHE 17:06:36.950 -> BOLSHE 17:06:37.277 -> BOLSHE 17:06:37.653 -> BOLSHE 17:06:37.980 -> BOLSHE 17:06:38.308 -> MUZIKI 17:06:38.637 -> MUZIKI 17:06:39.058 -> MUZIKI 17:06:39.385 -> MUZIKI 17:06:39.714 -> MUZIKI 17:06:40.088 -> MUZIKI 17:06:40.463 -> ******** 17:06:40.791 -> ******** 17:06:41.119 -> ******** 17:06:41.493 -> ******** 17:06:41.822 -> ********

Хотите что бы это постоянно крутилось у вас перед глазами на дисплее?

Код 2A это радиотекст. Если вы ожидаете, что там будет крутиться название композиции и имя исполнителя звучащей в эфире композиции, то вас скорее всего ждет разочарование. В моем городе станции передают радиотекст такого вида:

16:58:18.327 -> RDX: RUSSKOE RADIO IZHEVSK 93-90-90

или

17:00:49.973 -> RDX: Radio Vera - Svetoe Radio

или опять же:

17:05:07.856 -> RDX: ENERGY IZHEVSK : 96,2FM : REKLAMA 93-90-90

Так что если вам нужно узнать название композиции, то без Шазама опять не обойтись.

Самый полезный на мой взгляд код - это 4A, с которым передается текущее время и дата:

17:06:52.067 -> 0xD4A0 0x8D80 0x7760 0x4541 0xCCF6 0xD148 time: 13:5, offset: +8 Date: 3-6-2020 3

Пакеты с датой передается один раз в минуту. Они не дублируются, в отличии от радиотекста. Поэтому, если вы его пропустили, то нужно ждать следующего, еще минуту.

Здесь offset это часовой пояс который задается в получасах. Т.е. +8 означает часовой пояс +4 к Гринвичу. Дата задается в виде модифицированной юлианской даты - MJD. Юлианская дата не имеет никакого отношения к юлианскому календарю, не путайте эти два понятия. Перевод MJD в обычные дни, года и месяца, достаточная непростая процедура для 8-битного микроконтроллера, особенно, если ваша программа на чистом ассемблере. Я уложился примерно в 400 ассемблерных строк или около 900 байт. Много это или мало, судите сами.

Замечу, что Европа Плюс - это единственная из более 20 станций в моем городе, которая передает корректное время и дату через RDS. Но данные все-равно идут со смещением на две минуты. Нужна ли вам такая точность, решайте сами.

Чаще всего, передаваемое время выглядит так:

17:59:26.983 -> 0x54A8 0x8B80 0x7728 0x4161 0xCCF7 0x2800 time: 18:32, offset: +0 Date: 3-6-2020 3

Смещение на три минуты.

18:02:34.253 -> 0x54AF 0x9380 0x7730 0x4141 0xCCF7 0x2980 time: 18:38, offset: +0 Date: 3-6-2020 3

Смещение на полчаса.

18:04:40.346 -> 0x54B7 0x8B80 0x7827 0x4121 0xCCF6 0xDE08 time: 13:56, offset: +8 Date: 3-6-2020 3

Смещение на десять минут.

18:07:23.472 -> 0xD4BB 0x9180 0x7707 0x4161 0xCCF6 0xE248 time: 14:9, offset: +8 Date: 3-6-2020 3

Смещение на пятнадцать минут.

Ну и так далее.

16) Преобразование даты из MJD формата в дни, года и месяцы

Самый сложный момент в работе с RDS, это то, что дата передается в формате модифицированной юлианской даты - MJD, которая является 17-битным чистом. Для ее преобразования в дни, года и месяцы, от нас потребуется, ни много ни мало, библиотека арифметических операций для 32/24 битных чисел (!).

Вариантов модифицированной юлианской даты существует несколько, в данном случае, это разность текущей юлианской даты с юлианской датой на 1 января 1900 года. Юлианская дата (пишется JD) - это количество дней начиная с кого-то события и она широко используется в астрономии. MJD - это количество дней с 1 января 1900 года. Самый простой способ вычислить день недели какой-либо произвольной даты - это перевести дату в юлианскую, и взять остаток от деления этой даты на семь.

Для перевода даты из MJD формата в дни, месяцы и года, в описании стандарта RDS "EN50067. Specification of the radio data system (RDS) for VHF/FM sound broadcasting in the frequency range from 87,5 to 108,0 MHz. April 1998. имеется приложение (annex) G, где приведены все необходимые формулы:

Где Y, M, D, WD соответственно год, месяц, день месяца, день недели. С помощью алгебраических преобразований, избавимся от операций с дробными числами и сделаем так, что бы в знаменателях были 16-битные числа. После этого, переведем все константы в шестнадцатеричные числа.

Формулы ниже записаны в формате MathML. Firefox может показывать их без установки каких-либо дополнений. Для браузеров Chrome и Opera потребуется поставить расширение MathML.

Выполняем преобразования для первой формулы:

Y = I N T [ M D J 15078 , 2 365 , 25 ] = I N T [ M D J 100 1507820 36525 ] = I N T [ M D J 100 1507820 487 75 ] = I N T [ M D J 0x64 0x1701EC 0x1E7 0x4B ]

Тоже самое сделаем для второй формулы:

M = I N T { [ M J D 14956 , 1 I N T ( Y 365 , 25 ) ] 30 , 6001 } = I N T { [ M J D 10 149561 I N T ( Y 36525 100 ) 10 ] 1000 306001 } = I N T { [ ( M J D I N T ( Y 36525 100 ) ) 10 149561 ] 1000 9871 31 } = I N T { [ ( M J D I N T ( Y 0x8EAD 0x64 ) ) 0xA 0x24839 ] 0x3E8 0x268F 0x1F }

Третья формула:

D = M J D 14956 I N T ( Y 365 , 25 ) I N T ( M 30 , 6001 ) = M J D 14956 I N T ( Y 36525 100 ) I N T ( M 306001 100 100 ) = M J D 0x3A6C I N T ( Y 0x8EAD 0x64 ) I N T ( M 0x4AB51 0x64 0x64 ) =

Итак, мы избавилось в формулах от вещественных чисел, а константы в знаменателях разложили на простые множители. Теперь, что бы произвести вычисления по полученным формулам, нам потребуется библиотека для арифметических операций с 32-бит и 24-битными числами.

17) Математическая библиотека для 32/24 битных операций. Операции сложения, вычитания и умножения

Сложение и вычитание многобайтных чисел на 8-битных микроконтроллерах я рассматривал еще в 2014-м году, на примере ATtiny13A (см. статью Дизассемблирование blink.hex). С алгоритмом можно ознакомиться в книге Юрий Ревич "Практическое программирование микроконтроллеров Atmel AVR на языке ассемблера" 2-е издание. 2011г.

На ассемблере STM8 реализация 32-битного сложения и вычитания выглядит так:

add_uint32_idx
    ; input parameters: x pointer of number1, y pointer of number2,
    ; difference address to x
    push a
    ld a,(3,x)
    add a,(3,y)
    ld (3,x),a
    ld a,(2,x)
    adc a,(2,y)
    ld (2,x),a
    ld a,(1,x)
    adc a,(1,y)
    ld (1,x),a
    ld a,(x)
    adc a,(y)
    ld (x),a
    pop a
    ret

sub_uint32_idx
    ; input parameters: x address of minued, y address of subtrahend,
    ; difference address to x
    push a
    ld a,(3,x)
    sub a,(3,y)
    ld (3,x),a
    ld a,(2,x)
    sbc a,(2,y)
    ld (2,x),a
    ld a,(1,x)
    sbc a,(1,y)
    ld (1,x),a
    ld a,(x)
    sbc a,(y)
    ld (x),a
    pop a
    ret

Подпрограммы построены на 8-битных инструкциях. И хотя в STM8 есть 16-битные операции сложения и вычитания, однако нужных нам операций сложения и вычитания с переносом нет. Поэтому пришлось обойтись 8-битным набором инструкций. В качестве параметров принимаются адреса в оперативной памяти содержащие операнды. В процессе выполнения подпрограммы, первый операнд затирается результатом вычисления.

Реализация умножения не намного сложнее. Во-первых, для того что бы успешно выполнить вычисления, нам будет достаточно умножения 24-битного числа на 8-битное, в результате чего будет появляться 32-битное число. Во-вторых, у нас имеется аппаратное умножение двух восьмибитных чисел. У учетом этих факторов, имеем:

а) 24-битное число NMK, где N, M и K - соответственно старший, средний и младшие байты числа.

б) 8-битное число I на которое нужно умножить 24-битное.

Для этого представим число NMK как многочлен:

N M K = N 2 16 + M 2 8 + K 2 0

Тогда, согласно дистрибутивному закону умножения имеем:

N M K I = ( N I ) 2 16 + ( M I ) 2 8 + ( K I ) 2 0

Т.о. мы свели умножение многоразрядного числа на 8-битный множитель к 8-битному умножению и операциям сложения 32-битных чисел.

Если представить алгоритм в виде схемы, то выглядеть он будет как-то так:

На картинке представлен только обобщенный алгоритм. В своей реализации я не использовал сложение 32-битных чисел, я сделал немного попроще. В цикле, я последовательно перемножал числа: K*I, M*I, N*I и нарастающим итогом складывал старший байт произведения текущей итерации с младшим байтом следующей итерации. Т.е. как-то так:

Не знаю, насколько получилось понятно, сама подпрограмма получилась такой:

uint24_mult:
    ; subroutine for multiply 24bit integer number with 8bit integer number
    ; 24bit number must have 4 bytes. highest byte not used in calculate
    ; input parameters: X - pointer for 32-bit unsigned number
    ;                   A - 8-bit unsigned scaler
    ; output parameter: X - pointer for result of multiply
    pushw y
    push a              ; save scaler
    push #three         ; three steps
    addw x,#three       ; calculate three bytes
    push #0
uint24_mult_loop:
    ld a,(x)            ; get current byte
    ld yl,a             ; y=a
    ld a,(3,sp)         ; get scaler
    mul y,a             ; multiply
    ld a,yl
    add a,(1,sp)        ; low byte of result multiply add with high byte previous step
    ld (x),a            ; write result
    decw x
    ld a,yh
    adc a,#0            ; add carry flag to high byte of result
    ld (1,sp),a
    dec (2,sp)
    jrne uint24_mult_loop   ; go to next step
    addw sp,#2
    pop a
    popw y
    ret

Я прокомментирую основные этапы выполнения подпрограммы. Начнем:

    pushw y
    push a              ; save scaler
    addw x,#three       ; calculate three bytes

В начале подпрограммы сохраняем регистры A и Y в стеке. Регистр Х не сохраняем, вместо этого прибавляем к нему число 3 - количество итераций цикла. По мере прохождения циклов, от него будет вычитаться по единице, и регистр восстановит свое исходное значение. Прибавлением числа 3 к регистру Х, мы устанавливаем указатель на младший байт числа, т.е на число К, если взглянуть на иллюстрацию выше. Идем далее:

    push #three         ; three steps
    push #0

В стеке размещаем две локальных переменных. Первая переменная, которая равна трем, будет служить счетчиком цикла. Во второй переменной будет храниться старший байт результата умножения 8-битных чисел. Назовем эту переменную - SUM. При первой итерации значение SUM будет равно нулю. Переменную на которую указывает регистр Х, которая содержит перемножаемое 24-битное число и которая возвращает результат умножения будем называть RET.

uint24_mult_loop:
    ld a,(x)            ; get current byte
    ld yl,a             ; y=a
    ld a,(3,sp)         ; get scaler
    mul y,a             ; multiply

Копируем число К в регистр Y, число I копируем в аккумулятор, после чего производим перемножение.

    ld a,yl
    add a,(1,sp)        ; low byte of result multiply add with high byte previous step
    ld (x),a            ; write result

Младший байт результата умножения складываем с переменной SUM, после чего записываем его в младший байт переменной RET.

    decw x

Переходим к следующему байту переменной RET.

    ld a,yh
    adc a,#0            ; add carry flag to high byte of result

Складываем старший байт результата умножения с флагом переноса от сложения младшего байта.

    ld (1,sp),a

После чего сохраняем его в переменной SUM до следующей итерации.

    dec (2,sp)

Уменьшаем счетчик на единицу:

    jrne uint24_mult_loop   ; go to next step

И если после этого не сравнялся с нулем переходим к следующей итерации. Иначе завершаем подпрограмму

Не всегда за набором ассемблерных операций можно увидеть все нюансы алгоритма. Поэтому я прямо укажу на них.

    ld (1,sp),a

Здесь сохраняется в локальной переменной SUM старший байт результата умножения, однако в результирующей переменной RET он сохранится лишь при следующей итерации. Это означает, что если в результате умножения 24-битного числа на 8-битное получится 32-битное число, то старший байт результата будет отброшен, и подпрограмма вернет некорректный результат. Диапазон значений для вычисления даты не предполагает такие цифры, но если вам нужно все-таки 32-битное число на выходе, то после jrne добавьте строку "ld (x),a".

    add a,(1,sp)        ; low byte of result multiply add with high byte previous step
    ld (x),a            ; write result
    decw x
    ld a,yh
    adc a,#0            ; add carry flag to high byte of result

Здесь мы в операции adc учитываем флаг переноса (C-флаг) от операции add. Однако между этими двумя инструкциями стоят еще три инструкции. К счастью, их выполнение не оказывает влияния на Carry-флаг.

    adc a,#0            ; add carry flag to high byte of result

Еще один скользкий момент. Здесь мы прибавляем к старшему байту флаг переноса, а не может ли при этом произойти его переполнение? Нет, не может. Даже если мы перемножим два максимально возможных числа 0xFF на 0xFF, до в результате получим 0xFE01, а сложение числа 0xFE с единицей никогда не создаст переполнения.

18) Математическая библиотека для 32/24 битных операций. Операция деления

В википедии имеется статья посвященная алгоритмам деления: Division algorithm. Интересными мне показались: бинарный алгоритм деления столбиком "Integer division (unsigned) with remainder", метод Ньютона-Рафсона и метод замены деления умножением "Division by a constant". Нам бы, пожалуй, идеально подошел последний метод т.к. в знаменателях у нас везде константы. Данный метод заменяет операцию деления на операции умножения и сдвига вправо. Т.е. в принципе, метод заменяет деление на произвольное число делением на степень двойки. Например, что бы разделить произвольное число на 487, следует умножить его на 2153, и затем результат сдвинуть вправо на 20 разрядов. В общем виде преобразование выглядит следующим образом:

K M K 2 n M 1 2 n = K 2 n M 2 n

Т.е. "магическое число" 2153 это частное от деления двойки в двадцатой степени на число 487.

Так получилось, что прежде чем лезть в википедию за готовыми алгоритмами, я решил написать свою реализацию алгоритма деления столбиком, который всем нам знаком со школы. Я хотел, чтобы алгоритм использовал имеющуюся в STM8 операцию 16-битного деления. В результате я "изобрел" еще одно колесо. Деление в STM8 в принципе не быстрое, поэтому использовать его не имело смысла. Однако, алгоритм замены деления на умножение приблизительный. Его точность зависит от степени двойки. Чем больше степень, тем выше точность алгоритма. Это потребует от нас введения дополнительной многоразрядной арифметики, которая нигде больше не потребуется. Необходимо будет написать подпрограммы многорязрядного сдвига, сложения, умножения двух многоразрядных чисел. Кроме того потребуется преобразование между числами различной разрядности. Еще, для вычисления остатка от деления, потребуется перемножать частное на знаменатель, и полученное число вычитать из числителя. Т.е. потребуется еще подпрограмма вычитания, да и вся эта процедура вычисления остатка тоже будет занимать место и время. Написанная же мною подпрограмма ничего этого не требует. Поэтому, после некоторых колебаний я решил оставить все как есть.

Совсем не обязательно вникать в алгоритм моего способа деления 24-разрядного числа на 16-битное (на самом деле на 15-битное), это довольно опосредованно относится к теме RDA5807m. Поэтому вы можете со спокойной совестью промотать этот раздел, я же чувствую себя обязанным дать необходимые пояснения.

Технически, операцию деления 24-битного числа на 16-битное в идеальном случае можно разбить, на две операции 16-битного деления. Покажу простой пример.

Допустим, нам нужно вычислить день недели для даты 12 июня 2137года. Через онлайн калькулятор мы рассчитываем MJD = 101746, или 0х18D72 в шестнадцатеричной системе. День недели рассчитывается по следующей формуле:

W D = [ ( M J D + 2 )   m o d   7   ] + 1

Т.о. нам нужно число 0х18D74 разделить на семь и получить остаток от деления.

Согласно алгоритму деления столбиком, сначала нам нужно выделить из старших разрядов делимого число, которое будет больше знаменателя. Для этого разделим наше число 0х18D74 на два поля, и будем его делить в два этапа по этим полям:

Тогда вычисление будет происходить по следующим формулам. Вычисление частного:

0x18D74 7 = 0x18D 7 2 8 + ( 0x18D   m o d   7 ) 2 8 + 0x74 7 = 0x38 2 8 + 0x500 + 0x74 7 = 0x3800 + 0x574 7 = 0x3800 + 0xC7 = 0x38C7

Т.о. мы разбили деление 24-битного числа на две операции деления 16-битных чисел и операции сложения.

Чтобы вычислить остаток от деления, нам нужно во втором слагаемом нашего выражения, вместо деления на семь, вычислить остаток от деления на семь. Т.е. как-то так:

0x18D74   m o d   7 = [ ( 0x18D   m o d   7 ) 2 8 + 0x74 ]   m o d   7 = [ 0x500 + 0x74 ]   m o d   7 = [ 0x574 ]   m o d   7 = 0x3

Если зеленую и синюю часть числа 0х18D74 представить как число AB, где A=0x18D*2^8, а B=0x74, то формулы обобщенно можно будет записать так. Вычисления частного:

AB C = A C 2 8 + ( A   m o d   C ) 2 8 + B C

Вычисление остатка:

AB   m o d   C = [ ( 0xA   m o d   C ) 2 8 + 0xB ]   m o d   C

Недостатком алгоритма является то, что мы вслепую разделили числа на старшие два байта и младший байт. Если с семеркой в знаменателе алгоритм работает как надо, то если в знаменатель поставить число 0х18D или большее, то алгоритм выдаст некорректный результат. Кроме того, если в результате вычисления числителя второго слагаемого получиться число больше 16-битного, то мы вернемся к тому же, с чего начали, т.е. задаче деления 24-битного числа на 16-битное.

Первую проблему я решаю с помощью сдвига числителя влево, до тех пор, пока в 23-м разряде не появится единица. Затем результат деления сдвигается на тоже количество разрядов вправо. Я специально в формулах разбил константы в знаменателях на числа, которые бы не превышали 215. В таком случае числитель всегда получается больше знаменателя, и он не уходит целиком в остаток. Вторую проблему я решаю с помощью рекурсии. Окончанием рекурсии будет получение 16-битного числителя, который будет делиться одной операцией DIVW.

Если попытаться представить преобразование формулой, то будет наверно как-то так:

AB C 2 n = A'B' C = A' C 2 8 n + ( A'   m o d   C ) 2 8 n + B' C

Тут нужно пояснить. Во-первых, в программе никаких вычислений степеней двойки не производится, запоминается только число n, т.е. количество разрядов, на которое сдвигается влево исходный числитель. Далее при вычислении множителя, в младший байт числа записывается ноль:

    push #0
    pushw x         ; save quotient in stack    |

Т.о. мы расширяем 16-битное число до 24-битного, и одновременно умножаем его на 28. Затем это число сдвигается на n разрядов вправо.

Во-вторых, число B' - это младший байт исходного числа у которого обнулены старшие n-разрядов.

В качестве примера, можно разделить числа 0x11F81A на 0x10AB. Частное от этого деления будет Q=0x0114, остаток R=0x2BE. Число n будет равно 3.

0x11F81A 0x10AB 0x11F81A 0x10AB 2 3 = 0x8FD8D0 0x10AB = 0x8FD8 0x10AB 2 5 + ( 0x8FD8   m o d   0x10AB ) 2 5 + ( 0x1A & 0x1F ) 0x10AB = 0x8 2 5 + 0xA80 2 5 + ( 0x1A ) 0x10AB = 0x100 + 0x15000 + 0x1A 0x10AB = 0x100 + 0x1501A 0x10AB

Здесь частое от деления 0x1501A/0x10AB будет =0x14, остаток =0x2BE. Складываем 0х100 + 0х14 и получаем 0x114 в частном, и 0х2BE в остатке.

В программе число 0x1F, на которое производится логическое умножение младшего байта исходного числителя, никак не вычисляется. Вместо этого, младший байт в цикле вначале сдвигается влево на 3 разряда, затем также на 3 разряда вправо. Математически это будет тождественно логическому умножению на 1F.

Для деления 0x1501A/0x10AB нам опять придется вызвать рекурсивно нашу подпрограмму деления, и в этом случае число n будет =7.

0x1501A 0x10AB 0x1501A 0x10AB 2 7 = 0xA80D00 0x10AB = 0xA80D 0x10AB 2 1 + ( 0xA80D   m o d   0x10AB ) 2 1 + ( 0x1A & 0x01 ) 0x10AB = 0x0A 2 1 + 0x015F 2 1 + ( 0x0 ) 0x10AB = 0x14 + 0x2BE + 0x0 0x10AB = 0x14 + 0x2BE 0x10AB

В этот раз, в числителе второго множителя выпадает 16-битное число 0x2BE, и его деление на знаменатель будет выходом из рекурсии.

Полагаю, что с алгоритмом мы разобрались. Давайте посмотрим на подпрограмму:

.uint24_div:
    ; subroutine for divide 24bit integer number by a 16bit integer number
    ; 24bit number(dividend) must have 4 bytes. highest byte not used in calculate
    ; input parameters: X - pointer for 32-bit unsigned number(Quotient)
    ;                   Y - 16-bit divisor
    ; output paremeter: X - pointer fo quotient 
    ;                   Y - remainder
    push a          ; save accumulator
    pushw x         ; save X registor  <---------
    pushw y         ; save Y registor           |
    ldw x,(x)
    tnzw x
    jrne div_begin
    ldw x,(3,sp)
    ldw x,(2,x)
    divw x,y
    pushw x
    ldw x,(5,sp)
    ld a,(1,sp)
    ld (2,x),a
    ld a,(2,sp)
    ld (3,x),a
    addw sp,#4
    popw x
    pop a
    ret
div_begin:
    ldw x,(3,sp)        ; load pointer
    ld a,(3,x)          ; low byte to accumulator
    push a              ; save low byte
    push #0             ; counter=0, local variable
div_left_shift:
    ld a,(1,x)
    jrmi div_main
    sll (3,x)
    rlc (2,x)
    rlc (1,x)
    inc (1,sp)
    sll (2,sp)
    jra div_left_shift
div_main:
    ldw x,(1,x)     ; load high value           |
    divw x,y        ; divide, step 1            |
    pop a           ; counter
    push #0
    pushw x         ; save quotient in stack    |
    push #0
    push #0
    pushw y
div_right_shift:
    tnz a
    jreq end_shift
    dec a
    srl (1,sp)
    rrc (2,sp)
    rrc (3,sp)
    srl (5,sp)
    rrc (6,sp)
    rrc (7,sp)
    srl (8,sp)
    jra div_right_shift
end_shift:
    ld a,(8,sp)
    add a,(3,sp)
    ld (3,sp),a
    ld a,(2,sp)
    adc a,#0
    ld (2,sp),a
    ld a,(1,sp)
    adc a,#0
    ld (1,sp),a
    push #0
    ldw x,sp
    incw x
    ldw y,(10,sp)
    call uint24_div
    ;push #0
    pushw y
    ldw x,sp
    addw x,#3
    ldw y,x
    addw y,#4
    call add_uint32_idx
    ldw y,(3,sp)
    ldw x,(14,sp)
    ldw (x),y
    ldw y,(5,sp)
    ldw (2,x),y

    ; end of subroutine
    popw y
    addw sp,#13
    pop a
    ret

Подпрограмму функционально можно разделить на несколько блоков.

1) Вначале идет проверка старшего полуслова числителя на ноль, и если он оказывается равен нулю, то следовательно числитель 16-битный, и тогда производится обычное 16-битное деление, возврат частного и остатка, и выход из рекурсии.

    ldw x,(x)
    tnzw x
    jrne div_begin
    ldw x,(3,sp)
    ldw x,(2,x)
    divw x,y
    pushw x
    ldw x,(5,sp)
    ld a,(1,sp)
    ld (2,x),a
    ld a,(2,sp)
    ld (3,x),a
    addw sp,#4
    popw x
    pop a
    ret

Если числитель все-таки 24-битный, то нам нужно будет числитель сдвинуть влево, до появления единицы в старшем разряде. Но перед этим, нам нужно будет выделить, и отдельно сохранить младший байт числителя.

div_begin:
    ldw x,(3,sp)
    ld a,(3,x)
    push a

Также заносим в стек ноль, это будет локальная переменная - счетчик сдвига, оно же число n из формул:

    push #0

2) Второй блок сдвигает 24-битный числитель влево, до появления единицы в 23-м разряде. Параллельно этому, операцией "sll (2,sp)" сдвигается влево младший байт числителя на тоже количество разрядов:

div_left_shift:
    ld a,(1,x)
    jrmi div_main
    sll (3,x)
    rlc (2,x)
    rlc (1,x)
    inc (1,sp)
    sll (2,sp)
    jra div_left_shift
div_main:

3) В третьем блоке вычисляется первое слагаемое путем деления:

div_main:
    ldw x,(1,x)     ; load high value           |
    divw x,y        ; divide, step 1            |

После чего вытаскиваем из стека число произведенных сдвигов влево и загружаем это число в аккумулятор. Он будет счетчиком еще одного цикла сдвига.

    pop a           ; counter

4) В четвертом блоке нам нужно будет сдвинуть частное и остаток от деления вправо. Частное у нас идет на первое слагаемое, из остатка мы получаем числитель второго слагаемого.

Частное мы расширим до 32 битного числа. Приведение числа к 32-битному формату необходимо, что бы на последнем этапе сложить оба слагаемых подпрограммой add_uint32_idx, которая работает с 32-битными цифрами. Но расширять число будем не записью нулей в старшие разряды, вместо этого, один нулевой байт запишем в младший байт числа, второй нулевой байт пойдет в старший байт. Математически, мы умножаем его на 2 8.

    push #0
    pushw x         ; save quotient in stack    |
    push #0

Аналогичным образом поступаем с остатком, но его мы оставим в 24-битном формате:

    push #0
    pushw y

Теперь сдвигаем частное, остаток и младший байт оригинального числителя вправо на n разрядов:

div_right_shift:
    tnz a
    jreq end_shift
    dec a
    srl (1,sp)
    rrc (2,sp)
    rrc (3,sp)
    srl (5,sp)
    rrc (6,sp)
    rrc (7,sp)
    srl (8,sp)
    jra div_right_shift
end_shift:

Теперь извлекаем из стека предварительно сохраненный первый байт изначально заданного числителя и суммируем его с остатком от деления, вычисляя таким образом числитель второго слагаемого:

end_shift:
    ld a,(8,sp)
    add a,(3,sp)
    ld (3,sp),a
    ld a,(2,sp)
    adc a,#0
    ld (2,sp),a
    ld a,(1,sp)
    adc a,#0
    ld (1,sp),a
    push #0

Первое слагаемое таким образом мы вычислили.

5) В пятом блоке остаток от деления складываем с младшим байтом оригинального числителя:

    ld a,(8,sp)
    add a,(3,sp)
    ld (3,sp),a
    ld a,(2,sp)
    adc a,#0
    ld (2,sp),a
    ld a,(1,sp)
    adc a,#0
    ld (1,sp),a

Далее расширяем число до 32-битного и через рекурсию вычисляем второе слагаемое:

    push #0
    ldw x,sp
    incw x
    ldw y,(10,sp)
    call uint24_div

6) В завершение, сохраняем пока остаток в стеке и затем складываем оба слагаемых:

    pushw y
    ldw x,sp
    addw x,#3
    ldw y,x
    addw y,#4
    call add_uint32_idx

После этого переписываем данные в выходные параметры, извлекаем остаток из стека в регистр Y, и выходим из подпрограммы:

   ldw y,(3,sp)
    ldw x,(14,sp)
    ldw (x),y
    ldw y,(5,sp)
    ldw (2,x),y

    ; end of subroutine
    popw y
    addw sp,#13
    pop a
    ret

Проверяем работу подпрограммы деления в отладчике:

Как видно, подпрограмма выдает корректный результат. Не следует конечно забывать, что код еще "сырой", не протестированный должным образом.

В целом, как я и говорил в начале, подпрограмма получилась несколько громоздкой. Если бы в CPU было "быстрое" аппаратное деление, то возможно такой алгоритм и имел бы смысл. Но мне все-равно было интересно реализовать его.

19) Подпрограмма преобразования даты

Теперь, когда мы решили все математические проблемы, остается написать подпрограмму, которая будет преобразовывать модифицированную юлианскую дату в года, дни и месяцы. Это довольно рутинный процесс на котором мне не хотелось бы подробно останавливаться, сама подпрограмма подробно прокомментирована. Хочу только сделать одно замечание по алгоритму. Когда я писал и отлаживал данную подпрограмму, я использовал две глобальные 32-битные переменные var1 и var2, и на них производил все вычисления. Соответсвенно адреса этих переменных можно было заменить меткой, и в тексте программы всегда было видно когда происходит обращение к первой или второй переменной. Но когда программа была уже отлажена, я решил,что будет лучше глобальные переменные заменить локальными. Соответственно программа теперь начинается с инструкции "subw sp,#8" которая освобождает в стеке место под эти две переменные. Но к сожалению, мы потеряли наглядность. Граница стека постоянно смещается, половина текста программы теперь состоит из пар ("магическое число" + sp). Поэтому в комментариях у меня подписано, где к какой переменной происходит обращение. Все остальное здесь довольно тривиально.

.calc_date
    ; output date to sp+3(8bit), month to sp+4(8bit), year to sp+5(8bit), day to sp+6(8bit)
    subw sp,#8
    ; ------ calculate WD --------------
    ldw y,(11,sp)
    ldw (1,sp),y
    ldw y,(13,sp)
    ldw (3,sp),y        ; mdj to var1
    clrw y              ; var2=2
    ldw (5,sp),y
    ldw y,#2
    ldw (7,sp),y        ; var2=2
    ldw x,sp
    incw x              ; pointer of var1
    ldw y,sp
    addw y,#5           ; pointer of var1
    call add_uint32_idx ; var1=mdj +2
    ldw y,#7
    call uint24_div     ; WD = var1 / 7
    ld a,yl
    push a              ; save WD
    ; ------ calculate Y' --------------
    ; mjd to var1
    ldw x,(12,sp)
    ldw (2,sp),x
    ldw x,(14,sp)
    ldw (4,sp),x
    ; multiply
    ldw x,sp
    addw x,#2
    ld a,#100
    call uint24_mult        ; var1 = mdj * 100
    ldw x,#$17
    ldw (6,sp),x
    ldw x,#$01ec
    ldw (8,sp),x            ; var2 =1507820
    ldw x,sp
    ldw y,sp
    addw x,#2
    addw y,#6
    call sub_uint32_idx     ; var1 -=var2
    ;var1 /= 36525, where  36525 = 487*75
    ldw y,#$1e7             ; =487
    call uint24_div
    ldw y,#75
    call uint24_div
    ld a, (5,sp)            ; save Y' in stack
    push a
    ; ------ calculate M' -------------
    ;clrw y                 ; var1 = mjd
    ldw x,(13,sp)
    ldw (3,sp),x
    ldw x,(15,sp)
    ldw (5,sp),x
    ; multiply
    ldw x,sp
    addw x,#3               ; set pointer var1
    ld a,#100
    call uint24_mult        ; var1 *= 100
    pushw x
    ldw x,#$16
    ldw (9,sp),x
    ldw x,#$d23a
    ldw (11,sp),x           ; var2 =1495610
    popw x
    ldw y,sp
    addw y,#7               ; set pointer var2
    call sub_uint32_idx     ; var1 -= var2
    clrw x
    ldw (7,sp),x
    ldw x,#$8ead            ; var2 = 36525
    ldw (9,sp),x
    ld a,(1,sp)             ; A = Y'
    ldw x,sp
    addw x,#7
    call uint24_mult        ; var2 *= Y'
    subw x,#4               ; x = var1, y = var2
    call sub_uint32_idx     ; var1 -= var2
    ldw y,#100              ; rounding
    call uint24_div         ; var1 =/ 100
    ld a,#100
    call uint24_mult        ; var1 *= 10000
    call uint24_mult
    ldw y,#$1f              ; var1 /= 306001, where 306001=31*9871
    call uint24_div
    ldw y,#$268f
    call uint24_div
    ld a,(3,x)
    push a                  ; save M'
    ; ------ calculate D -------------
    ldw y,(14,sp)
    ldw (x),y
    ldw y,(16,sp)
    ldw (2,x),y             ; var1 = mjd
    clrw y
    ldw (4,x),y
    ldw y,#$3a6c
    ldw (6,x),y             ; var2 =14956
    ldw y,x
    addw y,#4
    call sub_uint32_idx     ; var1 -=var2
    pushw x
    pushw y
    clrw x
    ldw (y),x
    ldw x,#$8ead            ; var2 =36525
    ldw (2,y),x
    exgw x,y                ; x = var2
    ld a,(6,sp)             ; A = Y'
    call uint24_mult        ; var2 = 36525 * Y'
    ldw y,#100
    call uint24_div         ; var2 /= 100
    popw y
    subw x,#4
    call sub_uint32_idx     ; var1 -=var2
    clrw x
    ldw (y),x
    ldw x,#$268f            ; var2 =9871
    ldw (2,y),x
    pushw y
    exgw x,y                ; x=var2
    ld a,(5,sp)             ; A = M'
    call uint24_mult        ; var2 = M' * 306001
    ld a,#$1f
    call uint24_mult
    ldw y,#100
    call uint24_div
    ldw y,#100
    call uint24_div         ; var2 /= 10000
    popw y
    popw x
    call sub_uint32_idx     ; var1 -= var2
    ld a,(3,x)
    push a                  ; save D
    ; ------ calculate K -------------
    ld a,(2,sp)             ; A= M'
    push #0                 ; K=0
    cp a,#14
    jrmi zeroK              ; if (M'<14) then K=0
    inc (1,sp)              ; else M'=1
zeroK:
    ; ------ calculate Y -------------
    ld a,(4,sp)             ; A=Y'
    add a,(1,sp)            ; A=Y'+K
    ld (4,sp),a             ; save Y
    ; ------ calculate M -------------
    dec (3,sp)
    pop a
    clrw x
    ld xl,a
    ld a,#12
    mul x,a
    ld a,xl
    push a
    ld a, (3,sp)
    sub a,(1,sp)
    ld (3,sp),a
    ; ---- save result ----------------
    ld a,(2,sp)             ; Day
    ld (16,sp),a
    ld a,(3,sp)             ; Month
    ld (17,sp),a
    ld a,(4,sp)
    ld (18,sp),a            ; Year
    ld a,(5,sp)
    ld (19,sp),a            ; Week Day
    ;----------------------------------
    addw sp,#13             ; aligning stack
    ret

В качестве примера примера можно привести преобразование даты из MJD=0xE661=58977 в 8 мая 2020 года:

Здесь результат возвращается к нам в стеке. Четверка плюс единица - это день недели пятница, 0x78=120 это год 1900 + 120 = 2020, восьмерка это число месяца, а пятерка - это месяц май.

20) Реализация чтения RDS сообщений

Для чтения RDS сообщений я добавил в программу две команды (выделено красным):

Available commands: * s-/s+ - seek down/up with band wrap-around * v-/v+ - decrease/increase the volume * b-/b+ - bass on/off * d-/d+ - debug print on/off * r-/r+ - print RDS raw log on/off * l-/l+ - print RDS messages on/off * mute/unmute - mute/unmute audio output * ww/jp/ws/es/50 - cahnge band to: World Wide/Japan/West Europe/East Europe/50MHz * c-/c+ - change space: 100kHz/200kHz/50kHz/25kHz * rst - reset and turn off * on - Turn On * ?f - display currently tuned frequency * ?q - display RSSI for current station * ?v - display current volume * ?m - display mode: mono or stereo * v=num - set volume, where num is number from 0 to 15 * t=num - set SNR threshold, where num is number from 0 to 15 * b=num - set soft blend threshold, where num is number from 0 to 31 * f=freq - set frequency, e.g. f=103.8 * ?|help - display this list

Команды r+/r- включают и выключают печать RAW лога RDS регистров RDA5807m. печать идет с высокой скоростью, интервал составляет всего 43 мс. Регистры печатаются в шестнадцатеричном формате. Для этого мне потребовалось добавить в модуль uart1.asm подпрограмму печати шестнадцатеричного числа 16-битного числа:

    ; ----------- print hex number----------------------
    ; input parameter: X register
.uart1_print_hex:
    pushw x
    pushw y
    push a
    ldw y, sp
    ldw 1ch,y
    clrw y
    push #0
hex_loop:
    ld a,#10h
    div x,a
    ld yl,a
    ld a,(hex_digit,y)
    push a
    tnzw x
    jrne hex_loop
    push #'x'
    push #'0'
    ldw x,sp
    incw x
    call uart1_print_str
    ldw y,1ch
    ldw sp,y
    pop a
    popw y
    popw x
    ret
hex_digit:
    STRING "0123456789ABCDEF"

Выглядит RAW-лог как-то так:

11:18:24.286 -> RAW RDS logging is ON 11:18:24.320 -> 0xD4AF 0x8380 0x7730 0x14B 0xE0CD 0x2020 11:18:24.353 -> 0x54AF 0x8380 0x7730 0x14B 0xE0CD 0x2020 11:18:24.419 -> 0xD4AF 0x8380 0x7730 0x148 0xE0CD 0x5241 11:18:24.485 -> 0xD4AF 0x8380 0x7730 0x149 0xE0CD 0x4449 11:18:24.518 -> 0x54AF 0x8380 0x7730 0x149 0xE0CD 0x4449 11:18:24.585 -> 0xD4AF 0x8380 0x7730 0x14A 0xE0CD 0x4F20 11:18:24.651 -> 0xD4AF 0x8380 0x7730 0x14B 0xE0CD 0x2020 11:18:24.718 -> 0x54AF 0x8380 0x7730 0x14B 0xE0CD 0x2020 11:18:24.751 -> 0xD4AF 0x8380 0x7730 0x148 0xE0CD 0x5241 11:18:24.817 -> 0xD4AF 0x8380 0x7730 0x149 0xE0CD 0x4449 11:18:24.884 -> 0x54AF 0x8380 0x7730 0x149 0xE0CD 0x4449 11:18:24.950 -> 0xD4AF 0x8380 0x7730 0x14A 0xE0CD 0x4F20 11:18:25.016 -> 0xD4AF 0x8380 0x7730 0x14B 0xE0CD 0x2020 11:18:25.083 -> 0x54AF 0x8380 0x7730 0x14B 0xE0CD 0x2020 11:18:25.116 -> 0xD4AF 0x8380 0x7730 0x148 0xE0CD 0x3130 11:18:25.182 -> 0xD4AF 0x8380 0x7730 0x149 0xE0CD 0x342E 11:18:25.249 -> 0x54AF 0x8380 0x7730 0x149 0xE0CD 0x342E 11:18:25.315 -> 0x54AF 0x8380 0x7730 0x14A 0xE0CD 0x3546 11:18:25.348 -> 0xD4AF 0x8380 0x7730 0x14B 0xE0CD 0x4D20 11:18:25.414 -> 0x54AF 0x8380 0x7730 0x14B 0xE0CD 0x4D20 11:18:25.481 -> 0x54AF 0x8380 0x7730 0x148 0xE0CD 0x3130 11:18:25.548 -> 0xD4AF 0x8380 0x7730 0x149 0xE0CD 0x342E 11:18:25.580 -> 0x54AF 0x8380 0x7730 0x149 0xE0CD 0x342E 11:18:25.647 -> 0x54AF 0x8380 0x7730 0x14A 0xE0CD 0x3546 11:18:25.713 -> 0xD4AF 0x8380 0x7730 0x14B 0xE0CD 0x4D20 11:18:25.779 -> 0x54AF 0x8380 0x7730 0x14B 0xE0CD 0x4D20 11:18:25.846 -> 0x54AF 0x8380 0x7730 0x148 0xE0CD 0x3130 11:18:25.912 -> 0xD4AF 0x8380 0x7730 0x149 0xE0CD 0x342E 11:18:25.945 -> 0x54AF 0x8380 0x7730 0x149 0xE0CD 0x342E 11:18:26.012 -> 0x54AF 0x8380 0x7730 0x14A 0xE0CD 0x3546 11:18:26.078 -> 0xD4AF 0x8380 0x7730 0x14B 0xE0CD 0x4D20 11:18:26.111 -> 0x54AF 0x8380 0x7730 0x14B 0xE0CD 0x4D20 11:18:26.177 -> 0x54AF 0x8380 0x7730 0x148 0xE0CD 0x3130 11:18:26.244 -> 0xD4AF 0x8380 0x7730 0x149 0xE0CD 0x342E 11:18:26.310 -> 0x54AF 0x8380 0x7730 0x149 0xE0CD 0x342E 11:18:26.376 -> 0x54AF 0x8380 0x7730 0x14A 0xE0CD 0x3546 11:18:26.410 -> 0xD4AF 0x8380 0x7730 0x14B 0xE0CD 0x4D20 11:18:26.476 -> 0x54AF 0x8380 0x7730 0x148 0xE0CD 0x3130 11:18:26.542 -> RAW RDS logging is OFF

Команды l+ и l- также читают RDS регистры RDA5807m, но не выводят их тупо на печать, а пытаются их декодировать и уже полученный радиотекст, или время с датой, выводят на печать.

Лог той же станции, в данном случае будет выглядеть так:

11:22:12.268 -> 104.5FM 11:22:12.666 -> 104.5FM 11:22:12.997 -> 104.5FM 11:22:13.329 -> 104.5FM 11:22:13.694 -> ADAM 11:22:14.025 -> ADAM 11:22:14.390 -> ADAM 11:22:14.722 -> ADAM 11:22:15.120 -> ADAM 11:22:15.451 -> ADAM 11:22:15.783 -> ADAM 11:22:16.147 -> ADAM 11:22:16.479 -> ADAM 11:22:16.844 -> ADAM 11:22:17.208 -> RADI 11:22:17.540 -> RADIO 11:22:17.871 -> RAO O 11:22:18.203 -> RADIO 11:22:18.634 -> DIDIO 11:22:18.966 -> RADIO 11:22:19.297 -> RADIO 11:22:19.662 -> RADIO 11:22:20.026 -> 0xD4AF 0x8780 0x7730 0x4141 0xCD22 0xBEC0 time: 11:59, offset: +0 Date: 25-6-2020 4 11:22:20.060 -> RADIO 11:22:20.457 -> RADIO 11:22:20.789 -> RADIO 11:22:21.121 -> 104.5FM 11:22:21.452 -> 104.5FM 11:22:21.850 -> 104.5FM 11:22:22.182 -> 104.5FM 11:22:22.513 -> 104.5FM 11:22:22.911 -> 104.5FM 11:22:23.243 -> 104.5FM 11:22:23.574 -> 104.5FM 11:22:23.906 -> 104.5FM 11:22:24.304 -> 104.5FM

Или, если взять Европу плюс, то у них такой лог пойдет:

11:26:01.687 -> 103.0 FM 11:26:02.019 -> REKLAMA 11:26:02.749 -> REKLAMA 11:26:03.080 -> REKLAMA 11:26:03.246 -> RDX: pa PLUS IZHEVSK 103.0 FM 11:26:03.776 -> REKLAMA 11:26:04.141 -> 775-779 11:26:04.804 -> 775-779 11:26:05.202 -> 775-779 11:26:05.434 -> RDX: Europa PLUS IZHEVSK 103.0 FM 11:26:05.865 -> 775-779 11:26:06.926 -> EUROPA 11:26:07.258 -> EUROPA 11:26:07.656 -> RDX: Europa PLUS IZHEVSK 103.0 FM 11:26:07.987 -> EUROPA 11:26:08.318 -> PLUS 11:26:09.048 -> PL*⸮ 11:26:09.379 -> PL⸮⸮ 11:26:10.109 -> PLUS 11:26:10.440 -> NOMER 1 11:26:10.507 -> RDX: pa PLUS IZHEVSK 103.0 FM 11:26:11.104 -> NOMER 1 11:26:11.501 -> NOMER 1 11:26:12.496 -> V ⸮⸮SSII 11:26:12.729 -> RDX: Europa PLUS IZHEVSK 103.0 FM 11:26:13.226 -> V RO⸮)II 11:26:13.557 -> V ROSSII 11:26:14.287 -> V ROSSII 11:26:14.618 -> ******** 11:26:14.751 -> 0x54A0 0x8980 0x7760 0x4541 0xCD22 0x7688 time: 7:26, offset: +8 Date: 25-6-2020 4 11:26:14.983 -> RDX: pa PLUS IZHEVSK 103.0 FM 11:26:15.415 -> ******** 11:26:15.746 -> ******** 11:26:16.476 -> ****,*** 11:26:16.807 -> BOLSHE 11:26:17.205 -> RDX: Europa PLUS IZHEVSK 103.0 FM 11:26:17.537 -> BOLSHE 11:26:17.868 -> BOLSHE 11:26:18.598 -> BOLSHE

Заметьте, что по сравнению с логом от 6-го июня, этой же станции, который я приводил ранее, они подвели свои часы. Если тогда расхождение было две минуты, сейчас же идет отставание всего на 14 секунд.

Обе команды выполняются вызовом подпрограммы log_rds:

 892 log_rds:
 893     pushw x
 894     pushw y
 895     push a
 896     btjf RDA_STAT,#6, log_rds_raw
 897     ld a,$26
 898     and a,#$f8
 899     cp a,#$40               ; 0x4A
 900     jreq log_rds_4a
 901     cp a,#$0                ; 0x0A
 902     jrne switch_rds_00
 903     jp log_rds_0a
 904 switch_rds_00:
 905     cp a,#$08               ; 0x0B
 906     jrne switch_rds_01
 907     jp log_rds_0a
 908 switch_rds_01:
 909     cp a,#$20               ; 0x2A
 910     jreq log_rds_2a
 911     jp end_rds_log
 912 ;log_rds_0a:
 913     ;push #$0a
 914     ;jra log_rds_raw
 915 ;   jra log_rds_0a
 916 ;log_rds_2a:
 917     ;push #$2a
 918     ;jra log_rds_raw
 919     ;print_str msg_radiotext
 920 ;   jp log_rds_2a
 921     jp end_rds_log
 922 
 923 log_rds_4a:
 924     ;push #$4a
 925 log_rds_raw:
 926     ldw y,#$20
 927 log_rds_loop:
 928     ldw x,y
 929     ldw x,(x)
 930     call uart1_print_hex
 931     ld a, #' '
 932     call uart1_print_char
 933     addw y,#2
 934     cpw y,#$2c
 935     jrne log_rds_loop
 936     btjt RDA_STAT,#6, log_rds_print_time
 937     jp end_raw_log
 938 log_rds_print_time:
 939     jp log_rds_print_4a
 940 ;   pop a
 941 ;   cp a,#$4a
 942 ;   jreq log_rds_print_4a
 943 ;   cp a,#$0a
 944 ;   jreq log_rds_print_0a
 945 ;   jp end_raw_log
 946 log_rds_2a:
 947     ld a,$27
 948     and a,#$0f
 949     tnz a
 950     jrne log_2a_idx_not_zero
 951     tnz $3f
 952     jreq log_2a_idx_zero
 953     print_str msg_rdx
 954     ldw x,#RDSTXT2A
 955     call uart1_print_str
 956     print_nl
 957     jra log_2a_idx_zero
 958 log_2a_wrong:
 959     call clr_rdstxt_2a
 960     jp end_rds_log
 961 log_2a_idx_not_zero:
 962     push a
 963     sub a,$3f
 964     cp a,#2
 965     pop a
 966     jrnc log_2a_wrong
 967     ld $3f,a
 968     sll a
 969     sll a
 970     jra jump_01
 971 log_2a_idx_zero:
 972     call clr_rdstxt_2a
 973 jump_01
 974     add a,#RDSTXT2A
 975     y=a
 976     ldw x,$28
 977     ldw (y),x
 978     ldw x,$2a
 979     ldw (2,y),x
 980     clr (4,y)
 981     ld a,$27
 982     and a,#$0f
 983     cp a,#$f
 984     jreq log_2a_print_radiotext
 985     jp end_rds_log
 986 log_2a_print_radiotext:
 987     print_str msg_rdx
 988     ldw x,#RDSTXT2A
 989     call uart1_print_str
 990     clr $3f
 991     jp end_raw_log
 992 log_rds_0a:
 993 ;log_rds_print_0a:
 994     ld a,$27            ; get index
 995     and a,#$3
 996     tnz a
 997     jrne log_rda_idx_not_zero
 998     call clr_rdstxt
 999     jra log_rda_idx_zero
1000 log_rds_wrong:
1001     call clr_rdstxt
1002     jp end_rds_log
1003 log_rda_idx_not_zero:
1004     push a
1005     sub a,$3e
1006     cp a,#2
1007     pop a
1008     jrnc log_rds_wrong
1009     ld $3e,a
1010     sll a
1011 log_rda_idx_zero:
1012     add a,#RDSTXT
1013     y=a
1014     ldw x,$2a
1015     ldw (y),x
1016     ld a,$27            ; get index
1017     and a,#$3
1018     cp a,#3
1019     jreq log_rda_idx_print
1020     jp end_rds_log
1021 log_rda_idx_print:
1022     ldw x,#RDSTXT
1023     call uart1_print_str
1024     clr $3e
1025 error_rds:
1026     jp end_raw_log
1027 log_rds_print_4a:
1028     print_str msg_time
1029     ; extract Hour
1030     ldw x,$29
1031     srlw x
1032     ld a,xl
1033     srl a
1034     srl a
1035     srl a
1036     x=a
1037     call uart1_print_num
1038     ld a,#':'
1039     call uart1_print_char
1040     ; extract Minutes
1041     ldw x, $2a
1042     sllw x
1043     sllw x
1044     ld a,xh
1045     and a,#$3f
1046     x=a
1047     call uart1_print_num
1048     print_str msg_offset
1049     btjf $2b,#5,negative_offset
1050     ld a,#'+'
1051     call uart1_print_char
1052     jra log_rds_print_offset
1053 negative_offset:
1054     ld a,#'+'
1055     call uart1_print_char
1056 log_rds_print_offset:
1057     ld a,$2b
1058     and a,#$1f
1059     x=a
1060     call uart1_print_num
1061     ldw x,$28
1062     srlw x
1063     btjt $27,#1,log_rds_error_date
1064     ld a,$27
1065     and a,#$03
1066     swap a
1067     sll a
1068     sll a
1069     sll a
1070     jrc log_rds_error_date
1071     push a
1072     ld a,xh
1073     or a,(1,sp)
1074     ld xh,a
1075     pop a
1076     cpw x,#$e661                ; 8 may 2020
1077     jrc log_rds_error_date
1078     pushw x
1079     print_str msg_is_date
1080     ;pushw x
1081     clrw x
1082     pushw x
1083     call calc_date
1084     pop a
1085     x=a
1086     call uart1_print_num
1087     ld a, #'-'
1088     call uart1_print_char
1089     pop a
1090     x=a
1091     call uart1_print_num
1092     ld a, #'-'
1093     call uart1_print_char
1094     pop a
1095     x=a
1096     addw x,#1900
1097     call uart1_print_num
1098     ld a, #' '
1099     call uart1_print_char
1100     pop a
1101     inc a
1102     x=a
1103     call uart1_print_num
1104     jra end_raw_log
1105 log_rds_error_date:
1106     print_str msg_error_date
1107 end_raw_log:
1108     print_nl
1109 end_rds_log:
1110     pop a
1111     popw y
1112     popw x
1113     ret

Подпрограмма в целом простая, почти линейная, но есть пара нюансов, о которых следует рассказать.

В оперативке зарезервировано два буфера под радиотекст:

  42 RDSTXT  equ $30
  43 RDSTXT2A equ $40

Очистка этих буферов производится подпрограммами clr_rdstxt и clr_rdstxt_2a:

1114 clr_rdstxt:
1115     pushw x
1116     push a
1117     ldw x, #{RDSTXT+8}
1118     ld a,#$20
1119 fill:
1120     decw x
1121     ld (x),a
1122     cpw x,#RDSTXT
1123     jrne fill
1124     clr $3e
1125     pop a
1126     popw x
1127     ret
1128 clr_rdstxt_2a:
1129     pushw x
1130     push a
1131     ldw x, #{RDSTXT2A+64}
1132     ld a,#$20
1133 fill_2a:
1134     decw x
1135     ld (x),a
1136     cpw x,#RDSTXT2A
1137     jrne fill_2a
1138     clr $3f
1139     pop a
1140     popw x
1141     ret

Первый буфер имеет размерность 8 байт, второй 64 байта. Они очищаются путем заполнения ASCII символом пробела с кодом 0х20. В переменных по адресам 0х3e и 0x3f расположены счетчики циклов, они обнуляются при очистки буферов.

Заполнение буферов происходит по следующему правилу. Индекс каждого нового символа должен быть равен текущему (т.е. мы приняли дубль) или превосходить его на единицу (приняли следующие по порядку символы). Если это правило нарушается, буфер признается испорченым.

Команды r+/r- и l+/l- различаются состоянием 6-го бита флаговой переменной RDA_STAT. В самом начале работы подпрограммы log_rds, проверяется состояние этого бита:

 892 log_rds:
 893     pushw x
 894     pushw y
 895     push a
 896     btjf RDA_STAT,#6, log_rds_raw

И если оно оказывается равно нулю, то происходит переход на цикл распечатки RDS регистров RDA5807m:

 925 log_rds_raw:
 926     ldw y,#$20
 927 log_rds_loop:
 928     ldw x,y
 929     ldw x,(x)
 930     call uart1_print_hex
 931     ld a, #' '
 932     call uart1_print_char
 933     addw y,#2
 934     cpw y,#$2c
 935     jrne log_rds_loop
 936     btjt RDA_STAT,#6, log_rds_print_time
 937     jp end_raw_log

После завершения цикла снова проверяется 6-й бит флаговой переменной RDA_STAT, и если она равна нулю, т.е. выполняется команда "r+", то осуществляется переход по метке "jp end_raw_log", где происходит выход из подпрограммы.

Кроме процесса выполнения команды "r+", регистры печатаются еще при выполнении команды "l+", когда поступил пакет 4A, т.е. время и дата. При выполнении команды "l+", в начале выполнения подпрограммы log_rds идет парсинг поступивших пакетов:

 897     ld a,$26
 898     and a,#$f8
 899     cp a,#$40               ; 0x4A
 900     jreq log_rds_4a
 901     cp a,#$0                ; 0x0A
 902     jrne switch_rds_00
 903     jp log_rds_0a
 904 switch_rds_00:
 905     cp a,#$08               ; 0x0B
 906     jrne switch_rds_01
 907     jp log_rds_0a
 908 switch_rds_01:
 909     cp a,#$20               ; 0x2A
 910     jreq log_rds_2a
 911     jp end_rds_log

Варианты здесь могут быть 0A, 0B, 2A и 4A. Если пакет не относиться ни к одной из этой групп, то происходит переход на выход из подпрограммы rds_log.

В случае если поступил пакет 4A, то после того как будут распечатаны RDS-регистры, следует парсинг из пакета времени, печатаются сообщение "time: ", далее значение времени, потом печатается сообщение "offset: ", и выводится часовой пояс в получасах. После всего этого вызывается подпрограмма cal_date которая преобразует дату из MDJ формата в год, месяц, дату и день недели:

1027 log_rds_print_4a:
1028     print_str msg_time
1029     ; extract Hour
1030     ldw x,$29
1031     srlw x
1032     ld a,xl
1033     srl a
1034     srl a
1035     srl a
1036     x=a
1037     call uart1_print_num
1038     ld a,#':'
1039     call uart1_print_char
1040     ; extract Minutes
1041     ldw x, $2a
1042     sllw x
1043     sllw x
1044     ld a,xh
1045     and a,#$3f
1046     x=a
1047     call uart1_print_num
1048     print_str msg_offset
1049     btjf $2b,#5,negative_offset
1050     ld a,#'+'
1051     call uart1_print_char
1052     jra log_rds_print_offset
1053 negative_offset:
1054     ld a,#'+'
1055     call uart1_print_char
1056 log_rds_print_offset:
1057     ld a,$2b
1058     and a,#$1f
1059     x=a
1060     call uart1_print_num
1061     ldw x,$28
1062     srlw x
1063     btjt $27,#1,log_rds_error_date
1064     ld a,$27
1065     and a,#$03
1066     swap a
1067     sll a
1068     sll a
1069     sll a
1070     jrc log_rds_error_date
1071     push a
1072     ld a,xh
1073     or a,(1,sp)
1074     ld xh,a
1075     pop a
1076     cpw x,#$e661                ; 8 may 2020
1077     jrc log_rds_error_date
1078     pushw x
1079     print_str msg_is_date
1080     ;pushw x
1081     clrw x
1082     pushw x
1083     call calc_date
1084     pop a
1085     x=a
1086     call uart1_print_num
1087     ld a, #'-'
1088     call uart1_print_char
1089     pop a
1090     x=a
1091     call uart1_print_num
1092     ld a, #'-'
1093     call uart1_print_char
1094     pop a
1095     x=a
1096     addw x,#1900
1097     call uart1_print_num
1098     ld a, #' '
1099     call uart1_print_char
1100     pop a
1101     inc a
1102     x=a
1103     call uart1_print_num
1104     jra end_raw_log

Дата проверяется на достоверный диапазон, дабы она не превышала 2038 год, и была не меньше 8 мая 2020 года.

При обработке пакетов 0A и 0B, сначала извлекается индекс пакета, и если он равен нулю, буфер очищается, после чего в него заносятся полученные данные. Если индекс не равен нулю, то он сравнивается с предыдущим и если их разница не равна нулю или единицы, весь буфер так же сбрасывается. В противном случае данные записываются в буфер согласно индексу. Если индекс равен равен, т.е. максимально возможному, то сообщение выводиться на печать.

 992 log_rds_0a:
 993 ;log_rds_print_0a:
 994     ld a,$27            ; get index
 995     and a,#$3
 996     tnz a
 997     jrne log_rda_idx_not_zero
 998     call clr_rdstxt
 999     jra log_rda_idx_zero
1000 log_rds_wrong:
1001     call clr_rdstxt
1002     jp end_rds_log
1003 log_rda_idx_not_zero:
1004     push a
1005     sub a,$3e
1006     cp a,#2
1007     pop a
1008     jrnc log_rds_wrong
1009     ld $3e,a
1010     sll a
1011 log_rda_idx_zero:
1012     add a,#RDSTXT
1013     y=a
1014     ldw x,$2a
1015     ldw (y),x
1016     ld a,$27            ; get index
1017     and a,#$3
1018     cp a,#3
1019     jreq log_rda_idx_print
1020     jp end_rds_log
1021 log_rda_idx_print:
1022     ldw x,#RDSTXT
1023     call uart1_print_str
1024     clr $3e
1025 error_rds:
1026     jp end_raw_log

Обработка пакета радиотекста 2A происходит аналогичным образом, с той разницей, что в данном пакете передается по четыре символа вместо двух, а максимальная длина строки может включать в себя 64 символа. Соответственно сообщение выводится на печать при получении 16-го по счету пакета.

 946 log_rds_2a:
 947     ld a,$27
 948     and a,#$0f
 949     tnz a
 950     jrne log_2a_idx_not_zero
 951     tnz $3f
 952     jreq log_2a_idx_zero
 953     print_str msg_rdx
 954     ldw x,#RDSTXT2A
 955     call uart1_print_str
 956     print_nl
 957     jra log_2a_idx_zero
 958 log_2a_wrong:
 959     call clr_rdstxt_2a
 960     jp end_rds_log
 961 log_2a_idx_not_zero:
 962     push a
 963     sub a,$3f
 964     cp a,#2
 965     pop a
 966     jrnc log_2a_wrong
 967     ld $3f,a
 968     sll a
 969     sll a
 970     jra jump_01
 971 log_2a_idx_zero:
 972     call clr_rdstxt_2a
 973 jump_01
 974     add a,#RDSTXT2A
 975     y=a
 976     ldw x,$28
 977     ldw (y),x
 978     ldw x,$2a
 979     ldw (2,y),x
 980     clr (4,y)
 981     ld a,$27
 982     and a,#$0f
 983     cp a,#$f
 984     jreq log_2a_print_radiotext
 985     jp end_rds_log
 986 log_2a_print_radiotext:
 987     print_str msg_rdx
 988     ldw x,#RDSTXT2A
 989     call uart1_print_str
 990     clr $3f
 991     jp end_raw_log

На этом мне бы хотелось закончить данную статью. Осталось не рассмотренным использование EEPROM для хранения строковых констант и для хранения результатов автопоиска FM станций. Когда я снова вернусь к этой теме, то планирую начать именно с этого. Емкость EEPROM у stm8s103 всего 640 байт. Размер программы сейчас 5.5 КБайт. Т.е. сильно сжать размер программы за счет EEPROM не получиться. Кроме того, потребуется переписать механизм управления драйвером FM приемника через конечный автомат, что бы управление у нас не было завязано только на UART интерфейсе, а чтобы мы могли через модули подключать управление через энкодер, кейпад, или через другой микроконтроллер. Конечный автомат позволит нам использовать несколько типов управления драйвером FM-приемника одновременно или по отдельности без переписывания кода управления.

Еще раз напоминаю, что посмотреть исходники, или скачать скомпилированные прошивки можно с портала GitLab по следующей ссылке: https://gitlab.com/flank1er/stm8_rda5807m.

21) Рефакторинг кода драйвера

Дальнейшей целью нашей работы будет превращение отладочного драйвера в рабочее устройство. Для этого нам предстоит "отвязать" управление драйвером от UART-интерфейса. Для начала будет достаточно, если мы реализуем управление через энкодер и отображение частоты станции через 4-x разрядный семисегментный индикатор. При этом мы не доложны потерять возможность управления драйвером через UART, если это будет необходимо.

Энкодер позволит нам переключать станции или частоты, а на индикаторе будет отображаться текущая частота.

В этой главе пока забудем об энкодере и индикаторе, и сосредоточимся на рефакторинге драйвера. Нам следует реализовать UART-интерфейс и управление драйвером отдельно друг от друга. Допустим, пусть управление драйвером будет производиться с помощью Switch - оператора, который на вход будет принимать номер команды драйвера(включение, поиск станции, изменение диапазона и т.д.) которую следует выполнить. И пусть у нас будет иметься парсер UART интерфейса, который текстовые команды будет прообразовывать в номера команд драйвера FM-приемника. Т.о. по приему команды через UART-интерфейс, парсер будет вызывать switch-оператор драйвера, и передавать ему номер команды которую следует выполнить.

Технически, это будет работать так. В главном цикле будем постоянно проверять, не поступила ли команда от UART-интерфейса. За это отвечает флаг READY. Если она поступила, то будем вызывать парсер (назовем его get_state), который скажет драйверу FM-приемника, какую команду ему следует выполнить.

На ассемблере это будет выглядеть так:

106 start:
107     btjf READY,#0,loop          ; if buffer not empty
108     call get_state
109     tnz a
110     jrmi start
111     sll a
112     x=a
113     ldw x,(chose_state,x)
114     call (x)

Здесь chose_state, - это список из подпрограмм которые может выполнять драйвер FM-приемника:

1064 .chose_state:
1065     DC.W cmd_mute,cmd_unmute, rst, cmd_bass_on, cmd_bass_off, cmd_mono, cmd_set_threshold
1066     DC.W cmd_set_soft_blend, band_ws, band_50M, band_ukv, band_jp, band_ww, debug_on, debug_off
1067     DC.W raw_on, raw_off, log_on, log_off, space_down, space_up,set_vol
1068     DC.W vol_down, vol_up, seek_down, seek_up, print_rssi, on_cmd, freq, volume
1069     DC.W get_freq, help

В предыдущей версии драйвера команды FM-приемника располагались прямо в главном цикле. Теперь они все оформлены как подпрограммы, и размещены в отдельном файле commands.asm. Файл большой, поэтому я его спрятал под спойлер:

stm8/
    #include "STM8S103F.inc"
    #include "rda5807.inc"

    extern rda5807m_control_write, rda5807m_write_register, rda5807m_get_readchan
    #IF USE_UART
    extern uart1_print_str, uart1_print_num,  uart1_print_char, uart1_print_hex
    #ENDIF
    extern delay, strfrac, calc_date, strnum, strcmp

    segment 'rom'
.cmd_mute:
    bres $10,#6                 ; clear DMUTE bit
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#UPDATE       ; update
    #IF USE_UART
    print_str msg_mute          ; print info message
    #ENDIF
    ret

.cmd_unmute:
    bset $10,#6                 ; set DMUTE bit
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#UPDATE       ; update
    #IF USE_UART
    print_str msg_unmute        ; print info message
    #ENDIF
    ret                         ; break

.cmd_bass_on:
    bset $10,#4                 ; set BASS flag
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#UPDATE       ; update
    #IF USE_UART
    print_str msg_bass_on       ; print info message
    #ENDIF
    ret

.cmd_bass_off:
    bres $10,#4                 ; reset BASS flag
    ldw x,$10                   ; x=REG_02 /CONTROL/
    pushw x
    call rda5807m_control_write ; write to REG_02
    addw sp,#2
    bset RDA_STAT,#UPDATE       ; update
    #IF USE_UART
    print_str msg_bass_off      ; print info message
    #ENDIF
    ret

.cmd_set_threshold:
    #IF USE_UART
    print_str msg_threshold
    #ENDIF
    ld a,$16                    ; load high byte REG_4 to accumulator
    and a,#$f0                  ; clear bitfield [14:10]
    ld $16,a                    ; return value from accumulator
    ldw x,#02
    call strnum                 ; get input value
    and a,#$0f                  ; set mask for bitfield [4:0]
    x=a
    #IF USE_UART
    call uart1_print_num
    #ENDIF
    or a,$16                    ; add with high byte REG_5
    ld $16,a
    ldw x,$16                   ; 
    pushw x
    push #5
    call rda5807m_write_register; write REG_4
    addw sp,#03
    bset RDA_STAT,#UPDATE       ; update
    #IF USE_UART
    print_nl
    #ENDIF
    ret                         ; break

.cmd_set_soft_blend:
    #IF USE_UART
    print_str msg_soft_blend
    #ENDIF
    ld a,$1a                    ; load high byte REG_7 to accumulator
    and a,#$83                  ; clear bitfield [14:10]
    ld $1a,a                    ; return value from accumulator
    ldw x,#02
    call strnum                 ; get input value
    and a,#$1f                  ; set mask for bitfield [4:0]
    x=a
    #IF USE_UART
    call uart1_print_num
    #ENDIF
    sll a                       ; left  shift to two bits
    sll a
    or a,$1a                    ; add with high byte REG_7
    ld $1a,a
    ldw x,$1a
    pushw x
    push #7
    call rda5807m_write_register; write REG_7
    addw sp,#03
    bset RDA_STAT,#UPDATE           ; update
    #IF USE_UART
    print_nl
    #ENDIF
    ret                         ; break

.on_cmd:
    #IF USE_UART
    print_str msg_on            ; print info message
    #ENDIF
    ldw x,$10
    ld a,xl
    or a,#RDA5807M_CMD_RESET    ; set RESET bit
    ld xl,a
    pushw x
    call rda5807m_control_write ; write to REG_02 /CONTROL/
    addw sp,#2
    ldw x,#$c10d                ; REG_02=0xC10D (Turn_ON + SEEK)
    pushw x
    call rda5807m_control_write ; write to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#UPDATE           ; update
    #IF USE_UART
    bset RDA_STAT,#PRINT            ; print freq
    #ENDIF
    ret

.vol_down:
    ld a,$17                    ; a=ram[0x17] (low byte REG_05 /VOLUME/)
    and a,#$0f                  ; mask
    jreq vol_min                ; if current volume is minimum(=0)
    ;------------
    #IF USE_UART
    print_str msg_volume        ; print info message  "volume="
    #ENDIF
    dec a                       ; volume -=1
    #IF USE_UART
    x=a
    call uart1_print_num        ; print volume
    print_nl                    ; print NL
    #ENDIF
    ;----------------
    ldw x,$16                   ; x=REG_05 /VOLUME/
    decw x                      ; volume down
    pushw x
    push #05
    call rda5807m_write_register; write X to REG_05 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#UPDATE       ; update
    ret                         ; break
vol_min:
    #IF USE_UART
    print_str msg_volume_min    ; print error message
    #ENDIF
    ret

.vol_up:
    ld a,$17                    ; a=ram[0x17] (low byte REG_05 /VOLUME/)
    and a,#$0f                  ; mask
    cp a,#$0f                   ; if (a==15)
    jreq vol_max                ; if current volume is maximum(=15)
    ;------------
    #IF USE_UART
    print_str msg_volume        ; print info message "volume="
    #ENDIF
    inc a
    #IF USE_UART
    x=a
    call uart1_print_num        ; print volume
    print_nl                    ; print NL
    #ENDIF
    ;----------------
    ldw x,$16                   ; x=REG_05 /VOLUME/
    incw x                      ; vlume up
    pushw x
    push #05
    call rda5807m_write_register; write X to REG_05 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#UPDATE       ; set "to update" flag
    ret                 ; break
vol_max:
    #IF USE_UART
    print_str msg_volume_max    ; print error message
    #ENDIF
    ret

.volume:
    ; get current Gain Control Bits(volume)
    #IF USE_UART
    print_str msg_volume        ; print "volume="
    #ENDIF
    ld a,$17                    ; load low byte of REG_5 /VOLUME/
    and a,#$0f                  ; mask
    x=a
    #IF USE_UART
    call uart1_print_num        ; print volume
    print_nl                    ; print NewLine
    #ENDIF
    ret                         ; break

.set_vol:
    ldw x,#02
    call strnum                 ; get integer part
    and a,#$0f                  ; mask parameter
    ldw x,$16                   ; x=REG_5 /VOLUME/
    push a
    ld a,xl
    and a,#$f0                  ; x=(x & 0xfff0)
    or a,(1,sp)                 ; x=(x | num)
    ld xl,a
    pop a
    pushw x
    push #05                    ; select REG_5 to write
    call rda5807m_write_register; write volume to REG_5 /VOLUME/
    addw sp,#03
    bset RDA_STAT,#UPDATE       ; update
    ret                         ; break

.debug_on:
    #IF USE_UART
    print_str msg_debug_on      ; print message: "Debug is ON"
    #ENDIF
    bset RDA_STAT,#DEBUG        ; set debug flag
    ret                         ; break

.debug_off:
    #IF USE_UART
    print_str msg_debug_off     ; print message: "Debug is OFF"
    #ENDIF
    bres RDA_STAT,#DEBUG        ; reset debug flag
    ret                         ; break

.raw_on:
    #IF USE_UART
    print_str msg_raw_rds_log_on    ; print message: "Debug is ON"
    #ENDIF
    bset RDA_STAT,#READRDS      ; set debug flag
    ret                         ; break

.raw_off:
    #IF USE_UART
    print_str msg_raw_rds_log_off   ; print message: "Debug is OFF"
    #ENDIF
    bres RDA_STAT,#READRDS      ; reset debug flag
    ret                 ; break

.log_on:
    #IF USE_UART
    print_str msg_rds_log_on    ; print message: "Debug is ON"
    #ENDIF
    bset RDA_STAT,#READRDS      ; set debug flag
    bset RDA_STAT,#DECODE       ; set log flag
    ret                         ; break

.log_off:
    #IF USE_UART
    print_str msg_rds_log_off   ; print message: "Debug is OFF"
    #ENDIF
    bres RDA_STAT,#READRDS      ; reset debug flag
    bres RDA_STAT,#DECODE       ; reset log flag
    ret                         ; break

.rst:
    bset $11,#1                 ; set RESET flag
    ldw x,$10                   ; load CONTROL reg to X
    pushw x
    call rda5807m_control_write ; write X to REG_02 /CONTROL/
    addw sp,#2
    ;reboot microcontroller
    ;print_str msg_reset            ; print message "Reset"
    ;mov RDA_STAT,#$10
    #IF WDOG
    mov IWDG_KR, #$cc;
    mov IWDG_KR, #$55;          ; unlock IWDG_PR & IWDG_RLR
    mov IWDG_PR, #6             ; =256
    mov IWDG_RLR, #1
    mov IWDG_KR, #$aa           ; lock IWDG_PR & IWDG_RLR
rst_loop:
    jra rst_loop
    #ENDIF
    ret
    ;jp start                   ; break

.help:
    #IF USE_UART
    print_str msg_ready         ; print help message
    #ENDIF
    ret                 ; break

.seek_up:
    bset $10,#1                 ; seek-up
    bset $10,#0                 ; seek enable
    ldw x,$10                   ; X = ram[0x10]
    pushw x
    call rda5807m_control_write ; write X to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#UPDATE       ; set "to update" flag
    #IF USE_UART
    bset RDA_STAT,#PRINT            ; print freq
    print_str msg_seekup        ; print info message
    #ENDIF
    ret                         ; break

.seek_down:
    bres $10,#1                 ; seek-down
    bset $10,#0                 ; seek enable
    ldw x,$10                   ; X = ram[0x10]
    pushw x
    call rda5807m_control_write ; write X to REG_02 /CONTROL/
    addw sp,#2
    bset RDA_STAT,#UPDATE       ; set "to update" flag
    #IF USE_UART
    bset RDA_STAT,#PRINT        ; print freq
    print_str msg_seekdown      ; print info message
    #ENDIF
    ret                         ; break

set_band:
    sll a
    sll a
    or a,SPACE
    or a,#$10                   ; set TUNE flag
    x=a
    pushw x
    push #03
    call rda5807m_write_register; write REG_3 /TUNE/
    addw sp,#03
    ldw x,#200                      ; delay(200ms)
    call delay
    jra seek_down

.band_ws:
    #IF USE_UART
    print_str msg_change_band
    print_str msg_band_eu
    #ENDIF
    clr a
    jra set_band

.band_ww:
    #IF USE_UART
    print_str msg_change_band
    print_str msg_band_ww
    #ENDIF
    ld a,#2
    jra set_band

.band_ukv:
    #IF USE_UART
    print_str msg_change_band
    print_str msg_band_ukv
    #ENDIF
    btjt $1a,#1,omit_50M_65M
    bset $1a,#1
    ldw x,$1a
    pushw x
    push #7
    call rda5807m_write_register; write REG_7
    addw sp,#03
omit_50M_65M:
    ld a,#3
    jra set_band

.band_jp:
    #IF USE_UART
    print_str msg_change_band
    print_str msg_band_jp
    #ENDIF
    ld a,#01
    jra set_band

.band_50M:
    #IF USE_UART
    print_str msg_change_band
    print_str msg_band_50M
    #ENDIF
    btjf $1a,#1,omit_65M_50M
    bres $1a,#1
    ldw x,$1a
    pushw x
    push #7
    call rda5807m_write_register; write REG_7
    addw sp,#03
omit_65M_50M:
    ld a,#3
    jp set_band

.space_up:
    ld a,SPACE
    inc a
    and a,#3
    push a
;-- CASE (space)
    sll a
    x=a
    ldw x,(case_recalc,x)
    jp (x)
case_recalc:
    DC.W space_100,space_200,space_50,space_25
space_100:
    #IF USE_UART
    print_str msg_space100
    #ENDIF
    call rda5807m_get_readchan
    srlw x
    srlw x
    jra space_recalc
space_25:
    #IF USE_UART
    print_str msg_space25
    #ENDIF
    call rda5807m_get_readchan
    sllw x
    jra space_recalc
space_50:
    #IF USE_UART
    print_str msg_space50
    #ENDIF
    call rda5807m_get_readchan
    sllw x
    sllw x
    jra space_recalc
space_200:
    #IF USE_UART
    print_str msg_space200
    #ENDIF
    call rda5807m_get_readchan
    srlw x
space_recalc:
    shiftX6
    ld a,BAND
    cp a,#4
    jrne not_50M_band
    ld a,#3
not_50M_band:
    sll a
    sll a
    or a,(1,sp)
    pushw x
    or a,(2,sp)
    or a,#$10                   ; set TUNE flag
    ld xl,a
    addw sp,#03
    pushw x
    push #03
    call rda5807m_write_register; write REG_3 /TUNE/
    addw sp,#03
    bset RDA_STAT,#UPDATE       ; to update
    #IF USE_UART
    bset RDA_STAT,#PRINT        ; print freq
    #ENDIF
    ret                         ; break

.space_down:
    ld a,SPACE
    dec a
    and a,#3
    push a
;-- CASE (space)
    sll a
    x=a
    ldw x,(case_recalc2,x)
    jp (x)
case_recalc2:
    DC.W space2_100,space2_200,space2_50,space2_25
space2_100:
    #IF USE_UART
    print_str msg_space100
    #ENDIF
    call rda5807m_get_readchan
    sllw x
    jra space_recalc
space2_200:
    #IF  USE_UART
    print_str msg_space200
    #ENDIF
    call rda5807m_get_readchan
    srlw x
    srlw x
    jra space_recalc
space2_50:
    #IF USE_UART
    print_str msg_space50
    #ENDIF
    call rda5807m_get_readchan
    srlw x
    jra space_recalc
space2_25:
    #IF USE_UART
    print_str msg_space25
    #ENDIF
    call rda5807m_get_readchan
    sllw x
    sllw x
    jra space_recalc

.freq:
    ; CHECK: if was recived "f=NUM.NUM" command
    ldw x,#02
    call strnum                 ; get integer part
    y=a                         ; y = integer part
    call strfrac                ; get fractional part
    push a                      ; fractional part to stack
    ld a,BAND
    sll a
    x=a
    ldw  x,(band_range,x)       ; x=low edge of current band
    pushw x
    subw y,(1,sp)               ; y = (integer part - low edge of current band)
    ld a,SPACE
    x=a
    ld a,(spaces,x)             ; load scaler
    mul y,a                     ; y = y * scaler
    ld a,(3,sp)
    x=a                         ; x = fractional part
    pushw y                     ; save y
    ld a,SPACE
    sll a
    y=a
    ldw y,(case_freq,y)
    jp (y)
case_freq:
    DC.W sp_is_0, sp_is_1, sp_is_2, sp_is_3
sp_is_3
    sllw x
    incw x
    ld a,#5
    div x,a
    jra sp_is_0
sp_is_2:
    ld a,#5
    div x,a                     ; x=x/5
    jra sp_is_0
sp_is_1:
    srlw x
sp_is_0:
    addw x,(1,sp)               ; x=x+y
    addw sp,#5
    push SPACE
    jp space_recalc

.print_freq:
    #IF USE_UART
    call rda5807m_get_readchan
    ld a,SPACE
    y=a
    ld a,(spaces,y)                 ; load current scaler
    y=a                             ; load scaler to Y reg
    divw x,y                        ; X = X / Scaler
    pushw x
    ld a,SPACE
    sll a
    x=a
    ldw x,(chose_space,x)
    jp (x)
chose_space:
    DC.W sp_0,sp_1,sp_2,sp_3
sp_1:
    sllw y
    jra sp_0
sp_2:
    ld a,#5
    mul y,a
    jra sp_0
sp_3:
    ld a,#25
    mul y,a
sp_0:
    print_str msg_freq              ; print X
    ld a,BAND
    sll a
    x=a
    ldw x,(band_range,x)
    addw x,(1,sp)
    addw sp,#2
    ;popw x
    ;addw x,band_range              ; X = X + low range of current band
    call uart1_print_num
    ld a,#'.'
    call uart1_print_char           ; print dot
    ldw x,y
    ld a,#3
    cp a,SPACE
    jrne print_fract
    cpw x,#100
    jrsge print_fract
    ld a,#'0'
    call uart1_print_char
print_fract:
    call uart1_print_num            ; print fractional part of frequency
    print_str msg_mhz
    btjf RDA_STAT,#DEBUG, print_freq_quit
;---print space --------
    print_str msg_space
    clrw x
    ld a,SPACE
    ld xl,a
    call uart1_print_num
    print_nl
;---print band ---------
    print_str msg_band
    clrw x
    ld a,BAND
    ld xl,a
    call uart1_print_num
    print_nl
;---print chan value ----
    print_str msg_chan
    call rda5807m_get_readchan
    call uart1_print_num
    print_nl
    #ENDIF
;---print stereo mode ----
    ;call print_stereo
.print_stereo:
    btjt $20,#2,stereo          ; check ST flag of 0x0a register
    #IF USE_UART
    print_str msg_mono          ; print info message
    #ENDIF
print_freq_quit
    ret
stereo:
    #IF USE_UART
    print_str msg_stereo        ; print info message
    #ENDIF
    ret

.get_freq:
    bset RDA_STAT,#UPDATE       ; update
    #IF USE_UART
    bset RDA_STAT,#PRINT        ; print freq
    #ENDIF
    ret

.cmd_mono
.print_rssi:                            ; print rssi
    #IF USE_UART
    ld a,$22                        ; load high byte 0x0B reg to accumulator
    srl a                           ; a = (a >> 1)
    print_str msg_rssi
    clrw x
    ld xl,a
    pushw x
    call uart1_print_num            ; print rssi
    popw x
    print_str msg_dbuv
    #ENDIF
    ret

.log_rds:
    #IF USE_UART
    pushw x
    pushw y
    push a
    btjf RDA_STAT,#DECODE, log_rds_raw
    ld a,$26
    and a,#$f8
    cp a,#$40               ; 0x4A
    jreq log_rds_4a
    cp a,#$0                ; 0x0A
    jrne switch_rds_00
    jp log_rds_0a
switch_rds_00:
    cp a,#$08               ; 0x0B
    jrne switch_rds_01
    jp log_rds_0a
switch_rds_01:
    cp a,#$20               ; 0x2A
    jreq log_rds_2a
    jp end_rds_log
;log_rds_0a:
    ;push #$0a
    ;jra log_rds_raw
;   jra log_rds_0a
;log_rds_2a:
    ;push #$2a
    ;jra log_rds_raw
    ;print_str msg_radiotext
;   jp log_rds_2a
    jp end_rds_log

log_rds_4a:
    ;push #$4a
log_rds_raw:
    ldw y,#$20
log_rds_loop:
    ldw x,y
    ldw x,(x)
    call uart1_print_hex
    ld a, #' '
    call uart1_print_char
    addw y,#2
    cpw y,#$2c
    jrne log_rds_loop
    btjt RDA_STAT,#6, log_rds_print_time
    jp end_raw_log
log_rds_print_time:
    jp log_rds_print_4a
;   pop a
;   cp a,#$4a
;   jreq log_rds_print_4a
;   cp a,#$0a
;   jreq log_rds_print_0a
;   jp end_raw_log
log_rds_2a:
    ld a,$27
    and a,#$0f
    tnz a
    jrne log_2a_idx_not_zero
    tnz $3f
    jreq log_2a_idx_zero
    print_str msg_rdx
    ldw x,#RDSTXT2A
    #IF USE_UART
    call uart1_print_str
    print_nl
    #ENDIF
    jra log_2a_idx_zero
log_2a_wrong:
    call clr_rdstxt_2a
    jp end_rds_log
log_2a_idx_not_zero:
    push a
    sub a,$3f
    cp a,#2
    pop a
    jrnc log_2a_wrong
    ld $3f,a
    sll a
    sll a
    jra jump_01
log_2a_idx_zero:
    call clr_rdstxt_2a
jump_01
    add a,#RDSTXT2A
    y=a
    ldw x,$28
    ldw (y),x
    ldw x,$2a
    ldw (2,y),x
    clr (4,y)
    ld a,$27
    and a,#$0f
    cp a,#$f
    jreq log_2a_print_radiotext
    jp end_rds_log
log_2a_print_radiotext:
    print_str msg_rdx
    ldw x,#RDSTXT2A
    call uart1_print_str
    clr $3f
    jp end_raw_log
log_rds_0a:
;log_rds_print_0a:
    ld a,$27            ; get index
    and a,#$3
    tnz a
    jrne log_rda_idx_not_zero
    call clr_rdstxt
    jra log_rda_idx_zero
log_rds_wrong:
    call clr_rdstxt
    jp end_rds_log
log_rda_idx_not_zero:
    push a
    sub a,$3e
    cp a,#2
    pop a
    jrnc log_rds_wrong
    ld $3e,a
    sll a
log_rda_idx_zero:
    add a,#RDSTXT
    y=a
    ldw x,$2a
    ldw (y),x
    ld a,$27            ; get index
    and a,#$3
    cp a,#3
    jreq log_rda_idx_print
    jp end_rds_log
log_rda_idx_print:
    ldw x,#RDSTXT
    call uart1_print_str
    clr $3e
error_rds:
    jp end_raw_log
log_rds_print_4a:
    print_str msg_time
    ; extract Hour
    ldw x,$29
    srlw x
    ld a,xl
    srl a
    srl a
    srl a
    x=a
    call uart1_print_num
    ld a,#':'
    call uart1_print_char
    ; extract Minutes
    ldw x, $2a
    sllw x
    sllw x
    ld a,xh
    and a,#$3f
    x=a
    call uart1_print_num
    print_str msg_offset
    btjf $2b,#5,negative_offset
    ld a,#'+'
    call uart1_print_char
    jra log_rds_print_offset
negative_offset:
    ld a,#'+'
    call uart1_print_char
log_rds_print_offset:
    ld a,$2b
    and a,#$1f
    x=a
    call uart1_print_num
    ldw x,$28
    srlw x
    btjt $27,#1,log_rds_error_date
    ld a,$27
    and a,#$03
    swap a
    sll a
    sll a
    sll a
    jrc log_rds_error_date
    push a
    ld a,xh
    or a,(1,sp)
    ld xh,a
    pop a
    cpw x,#$e661                ; 8 may 2020
    jrc log_rds_error_date
    pushw x
    print_str msg_is_date
    ;pushw x
    clrw x
    pushw x
    call calc_date
    pop a
    x=a
    call uart1_print_num
    ld a, #'-'
    call uart1_print_char
    pop a
    x=a
    call uart1_print_num
    ld a, #'-'
    call uart1_print_char
    pop a
    x=a
    addw x,#1900
    call uart1_print_num
    ld a, #' '
    call uart1_print_char
    pop a
    inc a
    x=a
    call uart1_print_num
    jra end_raw_log
log_rds_error_date:
    print_str msg_error_date
end_raw_log:
    print_nl
end_rds_log:
    pop a
    popw y
    popw x
    #ENDIF
    ret

.clr_rdstxt:
    pushw x
    push a
    ldw x, #{RDSTXT+8}
    ld a,#$20
fill:
    decw x
    ld (x),a
    cpw x,#RDSTXT
    jrne fill
    clr $3e
    pop a
    popw x
    ret

.clr_rdstxt_2a:
    pushw x
    push a
    ldw x, #{RDSTXT2A+64}
    ld a,#$20
fill_2a:
    decw x
    ld (x),a
    cpw x,#RDSTXT2A
    jrne fill_2a
    clr $3f
    pop a
    popw x
    ret

.get_state:
    ; parameters: A return of state
    clrw x                      ; ard of buffer: x=0
    ldw y,#str_mute
    call strcmp                 ; check incoming command
    jrne unmute
    clr a
    ret
unmute:
    ldw y,#str_unmute
    call strcmp                 ; check incoming command
    jrne nxt_rst
    ld a,#1
    ret
nxt_rst:
    ldw y,#cmd_rst
    call strcmp                 ; check incoming command
    jrne skip
    ld a,#2
    ret
skip:
    ldw x,#cmd_set
    subw x,#2
    clrw y
    ld a,#2
    ldw y,(y)
parser_loop:
    inc a
    cp a,#31
    jreq parser_quit_loop
    addw x,#2
    cpw y,(x)
    jrne parser_loop
    ret
parser_quit_loop
    ld a,yh
    cp a,#'?'
    jrne parser_fail
    ld a,#31
    ret
parser_fail:
    ld a,#$ff
    ret

.band_range:
    DC.W 87,76,76,65,50
.spaces:
    DC.B 10,5,20,40

    #IF USE_UART
msg_ready:
    STRING "RDA5807m is ready.",$0a,0
msg_mute:
    STRING "mute ON",$0a,0
msg_unmute:
    STRING "mute OFF",$0a,0
msg_bass_on:
    STRING "Bass On",$0a,0
msg_bass_off:
    STRING "Bass Off",$0a,0
msg_stereo:
    STRING "Stereo Mode",$0a,0
msg_mono:
    STRING "Mono Mode",$0a,0
msg_threshold
    STRING "Threshold = ",0
msg_soft_blend:
    STRING "soft blend threshold = ",0
msg_rssi:
    STRING "rssi: ",0
msg_dbuv:
    STRING " dBuV",$0a,0
msg_on:
    STRING "Turn on",$0a,0
msg_volume:
    STRING "volume=",0
msg_volume_max:
    STRING "volume is max",$0a,0
msg_volume_min:
    STRING "volume is min",$0a,0
msg_debug_on:
    STRING "Debug is ON",$0a,0
msg_debug_off:
    STRING "Debug is OFF",$0a,0
msg_raw_rds_log_on:
    STRING "RAW RDS logging is ON",$0a,0
msg_raw_rds_log_off:
    STRING "RAW RDS logging is OFF",$0a,0
msg_rds_log_on:
    STRING "RDS logging is ON",$0a,0
msg_rds_log_off:
    STRING "RDS logging is OFF",$0a,0
msg_seekup:
    STRING "Seek Up",$0a,0
msg_seekdown:
    STRING "Seek Down",$0a,0
msg_change_band:
    STRING "Change band to ",$00
msg_band_ww:
    STRING "World-Wide Band (76-108MHz)",$0a,0
msg_band_jp:
    STRING "Japan Band (76-91MHz)",$0a,0
msg_band_ukv:
    STRING "exUSSR Band (65-76MHz)",$0a,0
msg_band_50M:
    STRING "50MHz Band (50-65MHz)",$0a,0
msg_band_eu:
    STRING "West Band (87-108MHz)",$0a,0
msg_space200:
    STRING "Change space to 200kHz",$0a,0
msg_space100:
    STRING "Change space to 100kHz",$0a,0
msg_space50:
    STRING "Change space to 50kHz",$0a,0
msg_space25:
    STRING "Change space to 25kHz",$0a,0
msg_space:
    STRING "Space: ",0
msg_band:
    STRING "Band: ",0
msg_mhz:
    STRING " MHz",$0a,0
msg_freq:
    STRING "freq=",0
msg_chan:
    STRING "chan: ",0
msg_error_date:
    STRING " Incorrect Date", $0a,0
msg_is_date:
    STRING " Date: ",0
msg_rdx:
    STRING "RDX: ",0
msg_radiotext:
    STRING "RADIOTEXT: ", $0a,0
msg_offset
    STRING ", offset: ",0
msg_time
    STRING "time: ",0
    #ENDIF

str_mute:
    STRING "mute ON",0
str_unmute:
    STRING "mute OFF",0
cmd_rst:
    STRING "rst",0
cmd_set:
    STRING "b+b-?mt=b=ws50esjpwwd+d-r+r-l+l-c-c+v=v-v+s-s+?qonf=?v?f"
.chose_state:
    DC.W cmd_mute,cmd_unmute, rst, cmd_bass_on, cmd_bass_off, cmd_mono, cmd_set_threshold
    DC.W cmd_set_soft_blend, band_ws, band_50M, band_ukv, band_jp, band_ww, debug_on, debug_off
    DC.W raw_on, raw_off, log_on, log_off, space_down, space_up,set_vol
    DC.W vol_down, vol_up, seek_down, seek_up, print_rssi, on_cmd, freq, volume
    DC.W get_freq, help
    end

Здесь нет ничего интересного, весь код был рассмотрен ранее и он был скопирован практически без изменений из main.asm. Исключением являются появление блоков #IF - #ENDIF. Это блоки условной компиляции ассемблера STVD. Флаги условной компиляции задаются в свойствах проекта:

Я ввел два флага: WDOG и USE_UART. Первый флаг позволяет собрать проект с отключенным watchdog'ом, второй флаг позволяет собрать проект без командного UART-интерфейса.

Также я взял на себя смелость удалить сообщение подсказки команд:

msg_help:
    STRING "Available commands:",$0A
    STRING "* s-/s+         - seek down/up with band wrap-around",$0A
    STRING "* v-/v+       - decrease/increase the volume",$0A
    STRING "* b-/b+       - bass on/off",$0A
    STRING "* d-/d+       - debug print on/off",$0A
    STRING "* r-/r+       - print  RDS raw log on/off",$0A
    STRING "* l-/l+       - print RDS messages on/off",$0A
    STRING "* mute/unmute - mute/unmute audio output",$0A
    STRING "* ww/jp/ws/es/50 - cahnge band to: World Wide/Japan/West Europe/East Europe/50MHz",$0A
    STRING "* c-/c+       - change space: 100kHz/200kHz/50kHz/25kHz",$0A
    STRING "* rst         - reset and turn off",$0A
    STRING "* on          - Turn On",$0A
    STRING "* ?f          - display currently tuned frequency",$0A
    STRING "* ?q          - display RSSI for current station",$0A
    STRING "* ?v          - display current volume",$0A
    STRING "* ?m          - display mode: mono or stereo",$0A
    STRING "* v=num       - set volume, where num is number from 0 to 15",$0A
    STRING "* t=num       - set SNR threshold, where num is number from 0 to 15",$0A
    STRING "* b=num       - set soft blend threshold, where num is number from 0 to 31",$0A
    STRING "* f=freq      - set frequency, e.g. f=103.8",$0A
    STRING "* ?|help      - display this list",$0A,$00,

Я полагаю, что команды и так все помнят наизусть, а удаление этого сообщения освобождает нам один килобайт флеш-памяти. Я полностью убрал команду "help" которая печатала это сообщение и оставил лишь команду "?" которая теперь печатает сообщение:

RDA5807m is Ready.

Это полезно, когда надо убедиться, что микроконтроллер принимает ваши команды.

Т.о. вес прошивки с UART-интерфейсом уменьшился с 5.5 КБайт до 4.5 КБайт. Вес прошивки без UART-интерфейса сейчас составляет 2.4 КБайта.

В результате этих изменений, как можно догадаться, содержимое файла main.asm существенно сократилось, т.к. обработчики команд драйвера RDA5807 были перенесены из main.asm в модуль commands.asm. Это дало возможность отказаться от инструкций длинных переходов (jp, btj[ft]+jp), и вновь пользоваться короткими переходам в главном цикле. Однако этим я не ограничился. Я переработал логику главного цикла. Я отказался от использования двух флагов в RDA_STAT:

Отказ от флагов FIRST и RSSI позволил сократить число ветвлений и меток в главном цикле. Кроме того, я ввел новый заголовочный файл rda5807.inc где оставшиеся флаги получили буквенные аббревиатуры:

x=a MACRO
    clrw x
    ld xl,a
    MEND
y=a MACRO
    clrw y
    ld yl,a
    MEND
    #IF USE_UART
print_nl MACRO
    ld a, #$a
    call uart1_print_char
    MEND
print_str MACRO msg
    ldw x, #msg
    call uart1_print_str
    MEND
print_str_nl MACRO msg
    ldw x, #msg
    call uart1_print_str_nl
    MEND
    #ENDIF
shiftX6 MACRO
    push a
    ld a,#6
    LOCAL shift
shift:
    sllw x
    dec a
    jrne shift
    pop a
    MEND

LED equ 5
LEN equ 10
EOL equ LEN                 ; =Zero always  ; adr=0x0a
RDSTXT  equ $30
RDSTXT2A equ $40
;-------- Variables ----------------------
STR equ 0                   ; buffer[10bytes]
INDEX   cequ    {EOL+1}     ; 1 byte        ; adr=0x0b
READY   cequ    {INDEX+1}   ; 1 byte        ; adr=0x0c
RDA_STAT cequ   {READY+1}   ; 1 byte        ; adr=0x0d
BAND    cequ    {RDA_STAT+1}; 1 byte        ; adr=0x0e
SPACE   cequ    {BAND+1}    ; 1 byte        ; ard=0x0f
;-------- Constants ----------------------
RDA5807_CTRL equ $10
RDA5807_RDS cequ {RDA5807_CTRL+$10}
RDA5807M_SEQ_I2C_ADDRESS equ $20
RDA5807M_RND_I2C_ADDRESS equ $22
RDA5807M_CTRL_REG equ $02
RDA5807M_TUNER_REG equ $03
RDA5807M_CMD_RESET equ $0002
RDA5807M_RDS_H  equ RDA5807_RDS
;------- RDA_STAT -------------------------
UPDATE  equ 0   ; read registers of RDA5807m
PRINT   equ 1   ; print frequent
RSSI    equ 2   ; not use
DEBUG   equ 3   ; print debug  info
FIRST   equ 4   ; begin
READRDS equ 5   ; read rds
DECODE  equ 6   ; decoding RDS
;------------------------------------------

Сюда же были перемещены используемые макросы.

Оставшиеся флаги можно разделить на три группы. Первые - это флаги DEBUG и DECODE. Они практически не влияют на алгоритм выполнения главного цикла. Они включаются и выключаются командами через UART-интерфейс. Это флаги "долгожители", т.е. они переживают множество итераций главного цикла, пока их пользователь вручную не србросит через командный UART-интерфейс.

Флаг RDSREAD переключает режим холостого хода главного цикла. Это так же флаг "долгожитель".

Флаги UPDATE и PRINT устанавливаются обработчиками команд драйвера RDA5807. Они сбрасываются автоматически после выполнения команды драйвера.

Флаги UPDATE и PRINT задают порядок выполнения главного цикла. Для этого, я код из главного цикла разбил на подпрограммы:

154     ;------ subroutines --------------
155 UPDATE_RDS_REGISTERS:
156     #IF USE_UART
157     btjf RDA_STAT,#DEBUG,no_msg_update_rds
158     print_str msg_update_rds        ; print message: "Read RDS Registers"
159     #ENDIF
160 no_msg_update_rds
161     call rda5807m_rds_update
162     ret
163     ;---------------------------------
164 CHECK_TUNING:
165     btjt RDA5807M_RDS_H,#6, quit_update_rds
166     call LONG_DELAY
167     call UPDATE_RDS_REGISTERS
168     jra CHECK_TUNING
169 quit_update_rds
170     ret
171     ;-----------------------------------
172 UPDATE_CONTROL_REGISTERS:
173     #IF USE_UART
174     btjf RDA_STAT,#DEBUG,skip_msg_update_ctl
175     print_str msg_update_ctl        ; print message: "Read Control Registers"
176     #ENDIF
177 skip_msg_update_ctl:
178     call rda5807m_update            ; read CONTROL block of rda5807m reg[0x02 - 0x07]
179     #IF USE_UART
180     btjf RDA_STAT,#DEBUG,skip_msg_update_complete
181     print_str msg_update_complete   ; print message "Read Registers was Complete""=
182     #ENDIF
183 skip_msg_update_complete:
184     ret
185     ;btjf RDA_STAT,#FIRST,store_band_and_space
186     ;bres RDA_STAT,#FIRST
187     ;#IF USE_UART
188     ;print_str msg_ready
189     ;#ENDIF
190 ;---PRINT FREQUENCY of CURRENT STATION -----
191 ;store_band_and_space:
192     ;bres RDA_STAT,#UPDATE              ; if success, then 1) reset flag
193 ;-- write current space and band ---------  
194 STORE_BAND:
195     ld a,$13                        ; get low byte REG_03 /TUNER/
196     and a, #$03                     ; mask bitfield [1:0]
197     ld SPACE,a                      ; store SPACE value
198     ld a,$13                        ; get low byte REG_03 /TUNER/
199     and a,#$0c                      ; mask bitfield [3:2]
200     srl a                           ; (a>>1), right logical shift
201     srl a                           ; (a>>1), right logical shift
202     cp a,#3
203     jrne leave_50M
204     btjt $1a,#1,leave_50M
205     ld a,#4
206 leave_50M:
207     ld BAND,a                       ; store BAND value
208     ret
209     ;-------------------------------------
210 LONG_DELAY:
211     ldw x,#250
212     call delay
213     ret
214     ;-----------------------------------
215 IS_READY:
216     #IF USE_UART
217     print_str msg_ready
218     #ENDIF
219     ret
220     ;----------------------------------
221 Enable_Interrupt:
222     rim
223     ret

Здесь метки подпрограмм записаны заглавными буквами. Далее я записал массив с адресами данных меток:

235 states:
236     DC.W UPDATE_RDS_REGISTERS, UPDATE_CONTROL_REGISTERS, CHECK_TUNING, IS_READY
237     DC.W Enable_Interrupt, print_freq, print_rssi, STORE_BAND

Особняком стоят подпрограммы print_freq и print_rssi. Они размещены в модуле commands.asm. Далее, для этих подпрограмм, в модуле main.asm я ввел псевдонимы:

 11 RDS_REG_UPDATE  equ 0
 12 CTRL_REG_UPDATE equ 1
 13 CHECK_TUNE      equ 2
 14 READY_MSG       equ 3
 15 IRQ_ENABLE      equ 4
 16 FREQ_PRINT      equ 5
 17 RSSI_PRINT      equ 6
 18 SPACE_BAND      equ 7

И главный цикл теперь выглядит так:

106 start:
107     btjf READY,#0,loop          ; if buffer not empty
108     call get_state
109     tnz a
110     jrmi start
111     sll a
112     x=a
113     ldw x,(chose_state,x)
114     call (x)
115     clr INDEX                   ; INDEX=0
116     clr READY                   ; READY=0
117     #IF USE_UART
118     btjf RDA_STAT,#PRINT, if_update
119     push #RSSI_PRINT
120     push #FREQ_PRINT
121     bres RDA_STAT,#PRINT
122     #ENDIF
123 if_update:
124     btjf RDA_STAT,#UPDATE, loop
125     push #SPACE_BAND
126     push #CTRL_REG_UPDATE           ; update control register
127     push #CHECK_TUNE            ; check tuning
128     push #RDS_REG_UPDATE        ; update rds registers
129     bres RDA_STAT,#UPDATE
130 loop:
131     pop a
132     tnz a
133     jrmi loop_delay
134     sll a
135     x=a
136     ldw x,(states,x)
137     call (x)
138     jra loop
139 loop_delay:
140     #IF WDOG
141     mov IWDG_KR, #$aa               ; reset watchdog
142     #ENDIF
143     btjf RDA_STAT,#READRDS, delay_250
144     call rda5807m_rds_update
145     call log_rds
146     ldw x,#43
147     jra to_delay
148 delay_250:
149     ldw x,#250
150 to_delay:
151     call delay
152     push #$ff
153     jra start

Главный цикл работает как стековая машина. В начале выполнения цикла (метка loop), извлекается из стека число и, если оно попадает в диапазон приведенного выше массива (т.е. от нуля до семи), то выполняется соответствующая подпрограмма. В противном случае, цикл отрабатывает в холостую. Для холостого выполнения цикла в стек складывается число 0xff.

Вариантов работы главного цикла не так много. Давайте рассмотрим их.

  1. Первая итерация главного цикла при включении микроконтролера. В этом состоянии FM-приемник еще не настроен на какую-либо частоту, а условием работы драйвера является необходимость на что-то настроиться. Поэтому включение микроконтроллера является исключением. В этом случае драйвер считывает состояние регистров RDA5807, после чего переходит в холостой режим, т.е. в режим ожидания команды пользователя. Перед первой итерацией в стек заносятся следующие числа:
    101     push #$ff                   ; end of states
    102     push #IRQ_ENABLE            ; enable interrupt
    103     push #READY_MSG             ; print message "get ready"
    104     push #CTRL_REG_UPDATE       ; update control registers
    105     push #RDS_REG_UPDATE        ; update RDS registers
    предполагается, что (READY == 0). Т.о. выполнение происходит следующим образом: start=>loop=>RDS_REG_UPDATE=>CTRL_REG_UPDATE=>READY_MSG=>IRQ_ENABLE. После этого программа переходит на холостой цикл.
  2. Холостой цикл выполняется при (READY == 0 и RDSREAD == 0). В этом случае, выполнение цикла происходит так: start=>loop=>loop_delay=>delay_250
  3. Третий вариант случается когда читаются RDS сообщения. В этом случае постоянно происходит чтение RDS блока регистров и их парсинг с помощью подпрограммы log_rds. В рабочем варианте драйвера, этот вариант выполнения главного цикла скорее всего заменит собой цикл холостого хода. Этот вариант определяется следующим состоянием переменных: (READY == 0 и RDSREAD == 1)
  4. Четвертый, последний вариант выполнения главного цикла. В этом случае (READY == 1), соответственно вызывается подпрограмма get_state, после чего идет на выполнение какая-то команда драйвера.
    114     call (x)
    Команда драйвера в свою очередь может установить или не установить флаги UPDATE и PRINT. В случае их установки в стек заносится последовательность действий:
    117     #IF USE_UART
    118     btjf RDA_STAT,#PRINT, if_update
    119     push #RSSI_PRINT
    120     push #FREQ_PRINT
    121     bres RDA_STAT,#PRINT
    122     #ENDIF
    123 if_update:
    124     btjf RDA_STAT,#UPDATE, loop
    125     push #SPACE_BAND
    126     push #CTRL_REG_UPDATE           ; update control register
    127     push #CHECK_TUNE            ; check tuning
    128     push #RDS_REG_UPDATE        ; update rds registers
    129     bres RDA_STAT,#UPDATE
    поле чего сами флаги сбрасываются, а выполнение передается стековой машине.

Это все сложности. Не рассмотренной осталась лишь подпрограмма get_state, но она, в принципе, тривиальная. Еще хотел бы заметить, что в данной ревизии драйвер лишился механизма обработки ошибок I2C шины. Данные ошибки критические, т.к. с ними устройство не сможет работать в принципе. Самое простое в таких случаях зацикливать микроконтроллер, дожидаясь пока watchdog его перезагрузит. Но мне кажется, что правильнее будет выводить какое-либо сообщение через UART или на дисплей, на вроде "hardware error #2".

В следующей ревизии драйвера нужно будет добавить функцию автопоиска станций в текущем диапазоне, с записью их в EEPROM.

Полные исходники драйвера можно скачать с портала GitLab по ссылке https://gitlab.com/flank1er/stm8_rda5807m/-/tree/master/09_refactoring.

поделиться: