В статье пошагово описывается процесс написания драйвера для FM-приемника RDA5807m, где в качестве микроконтроллера используется STM8S103F3, а в качестве языка программирования - ассемблер со средой программирования STVD.
Структурно статью можно разделить три части. С одной стороны это статья об ассемблере 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 постепенно вытеснит низкоуровневое программирование в принципе, поэтому данная статья - это реверанс в сторону хардкорного программирования.
Полезные материалы по теме статьи:
Содержание:
I. Реализация командного интерфейса посредством UART
II. Минимальный драйвер RDA5807m
III. Драйвер с переключением диапазонов и интервалов частот
IV. Прием RDS данных (добавлено 25 июня 2020г.)
V. Подключение энкодера и дисплея к драйверу
Посмотреть исходники, или скачать скомпилированные прошивки можно с портала GitLab по следующей ссылке: https://gitlab.com/flank1er/stm8_rda5807m.
Для сборки устройства, нам понадобятся: сам модуль 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 см).
Написание кода, начнем не с 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, 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
Вот с квадратными скобочками хотелось бы разобраться поподробнее. Косвенная адресация не самая быстрая, т.к. сначала нужно "добыть" адрес источника/получателя в то время как при индексной адресации процессору сразу подсовывается регистр содержащий нужный адрес. НО. Когда-то во-времена процессоров 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. Такая вот логика.
Если вы планируете вносить свои изменения в обработчик прерывания, то вам скорее всего потребуется его отлаживать. У этого процесса есть рад особенностей.
Во-первых, для отладки следует посылать в микроконтроллер по одной литере, иначе вы установите флаг OR. Поэтому для отладки, в терминальной программе следует убрать символ окончания стоки:
В самой программе следует поставить точку останова в начале прерывания:
После этого можно запустить отладку, затем запустить программу на выполнение (восклицательный знак на панели отладки) и следом послать один символ через терминальную программу. Нас должно "выбросить" в режим трассировки. В окне "Memory" следует указать адрес 0x0:
После этого можно приступать к трассировке до выхода из прерывания. Если вам нужно лишь посмотреть результат в окошке "Memory" то вместо трассировки можно нажать значок "Continue" (подчеркнуто синим) и послать следующий символ через терминальную программу. Нас снова "выкинет" в режим трассировки, в окошке "Memory" изменённые байты будут выделены жирным шрифтом:
И так шаг за шагом. Если нужно будет послать символ окончания строки, то выбираете соответствующий символ в терминальной программе и посылаете пустую строку. Если нужно отладить флаг OR, то посылаете несколько символов за раз. В принципе, не сложно.
В общем виде, лог работы программы пока выглядит так:
Здесь, строки которые не вмещаются в 10 символов, обрезаются.
Теперь, когда у нас есть интерфейс, через который мы можем посылать команды на микроконтроллер, нам нужны будут функции (подпрограммы)для обработки строк, чтобы эти команды были понятны управляющей программе микроконтроллера. Нам нужны будут функции сравнения строк и извлечения чисел из строки. Для тестирования этих подпрограмм, мы напишем что-то вроде прототипа управляющей программы, которая будет принимать по 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".
В этой главе мы подключим 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 была перед глазами, я скопировал ее из прошлогодней статьи и поместил под спойлер:
показать карту регистров 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
На этом пока все. Хочу заметить, что при отладке, когда вы сбрасываете программу, имейте в виду, что RDA5807m при этом не сбрасывается, а сохраняет свое состояние. Т.е. подавать команду на включение нужно лишь единожды.
На данном этапе размер прошивки равняется 2404 байт.
Теперь нужно реализовать печать частоты текущей станции и переключение тюнера на указанную частоту, чтобы можно было переключаться между станциями в произвольном порядке. Частоту станции при этом можно указывать вручную через 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 можно посмотреть под спойлером.
показать код main.asmstm8/ #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
RDA5807 может работать со следующими диапазонами частот:
Т.о. с помощью переключения диапазонов RDA5807 может принимать сигнал с частот начиная с 50 МГц по 108 МГц. В документации УКВ диапазон назван восточно-европейским, а FM диапазон - западным. В свое время частоты под диапазон УКВ выделялись между телевизионными каналами. Кроме того, советский УКВ имел полярную модуляцию, но если верить википедии, сейчас в таком формате никто не вещает "так как приёмники, способные принимать такие стереопрограммы, много лет не производятся". В прошлом году, в стране происходило отключение вещания аналогового ТВ, частоты соответственно начали освобождаться. В частности, частоту 50МГц планируют передать радиолюбителям. Как бы то ни было, сейчас во всех диапазонах, за исключением FM, стоит тишина, там никто не вещает. Отсюда возникает вопрос - для чего нам писать программу для работы с этими диапазонами?
В начале, ради сокращения текста статьи, я не хотел добавлять функционал переключения диапазонов и принимаемых интервалов к драйверу. Единственный рабочий диапазон - это FM, а станции идут с шагом в 100 кГц. Следовательно, установок по-умолчанию вполне достаточно. НО. Мне показалось, что без этого драйвер будет не полноценным. Во-вторых, если в моем городе, в эфире, за исключением FM-диапазона, стоит тишина, это не означает, что в вашем городе будет так же. В-третьих, это может быть полезно. Например, можно сделать "уоки-токи" на неиспользуемых частотах. Передатчик для УКВ диапазона собирается "на коленке".
Код переключения диапазонов и интервалов частот будет отнимать немногим более килобайта места на флеше, что на мой взгляд вполне приемлемо. Общий размер прошивки равняется 3651 байтам. Не забываем, что одним килобайтом мы можем легко пожертвовать удалив подсказку по командам "msg_help".
В этот раз, в интерфейс работы с драйвером были внесены некоторые изменения, о которых я хотел бы рассказать.
Во-первых, при включении микроконтроллера должно появиться приветствие следующего вида:
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
В данный момент еще остались нереализованными команды: 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
Используемая память микроконтроллера на данном этапе выглядит так:
По сравнению с прошлым разом добавились массивы для хранения содержимого регистров 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 сбрасывается, и более не используется.
К сожалению, я не сразу догадался использовать псевдонимы для флагов в программе. Поэтому данные имена условные, а в программе вместо них используются "магические числа".
Введение флагов 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"
Теперь поговорим о том, как собственно реализуется переключение диапазонов и интервалов. Начнем с интервалов.
Переключение интервалов осуществляется командами "с+" и "с-". Для пересчетов интервалов, нам нужно взять текущее значение CHAN, умножить или разделить это значение на два или четыре. После этого следует перенастроить тюнер на новые параметры. Интервалы идут в таком порядке: 100кГц, 200кГц, 50кГц, 25кГц. Для примера, если нам требуется переключить интервал с 100кГц на 200кГц, то для этого нужно: а)взять текущее значение CHAN; б) разделить его надвое; в) записать в RDA5807 новое значение CHAN и номер интервала; г) запустить тюнер на перенастройку.
В теории все выглядит несложно. В переключением диапазонов еще проще. Перезаписываем новый диапазон в RDA5807, и запускаем тюнер на автонастройку.
На практике нам также придется переписать код печати частоты и код команды установки частоты "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
И на этом, в принципе, пока все.
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.
Прежде чем приступать к описанию 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'а перезагружает сам микроконтроллер.
Возможно баг проявляется только у меня, но все же я подумал, что будет лучше рассказать о нем.
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
Смещение на пятнадцать минут.
Ну и так далее.
Самый сложный момент в работе с 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.
Выполняем преобразования для первой формулы:
Тоже самое сделаем для второй формулы:
Третья формула:
Итак, мы избавилось в формулах от вещественных чисел, а константы в знаменателях разложили на простые множители. Теперь, что бы произвести вычисления по полученным формулам, нам потребуется библиотека для арифметических операций с 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 как многочлен:
Тогда, согласно дистрибутивному закону умножения имеем:
Т.о. мы свели умножение многоразрядного числа на 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 с единицей никогда не создаст переполнения.
В википедии имеется статья посвященная алгоритмам деления: Division algorithm. Интересными мне показались: бинарный алгоритм деления столбиком "Integer division (unsigned) with remainder", метод Ньютона-Рафсона и метод замены деления умножением "Division by a constant". Нам бы, пожалуй, идеально подошел последний метод т.к. в знаменателях у нас везде константы. Данный метод заменяет операцию деления на операции умножения и сдвига вправо. Т.е. в принципе, метод заменяет деление на произвольное число делением на степень двойки. Например, что бы разделить произвольное число на 487, следует умножить его на 2153, и затем результат сдвинуть вправо на 20 разрядов. В общем виде преобразование выглядит следующим образом:
Т.е. "магическое число" 2153 это частное от деления двойки в двадцатой степени на число 487.
Так получилось, что прежде чем лезть в википедию за готовыми алгоритмами, я решил написать свою реализацию алгоритма деления столбиком, который всем нам знаком со школы. Я хотел, чтобы алгоритм использовал имеющуюся в STM8 операцию 16-битного деления. В результате я "изобрел" еще одно колесо. Деление в STM8 в принципе не быстрое, поэтому использовать его не имело смысла. Однако, алгоритм замены деления на умножение приблизительный. Его точность зависит от степени двойки. Чем больше степень, тем выше точность алгоритма. Это потребует от нас введения дополнительной многоразрядной арифметики, которая нигде больше не потребуется. Необходимо будет написать подпрограммы многорязрядного сдвига, сложения, умножения двух многоразрядных чисел. Кроме того потребуется преобразование между числами различной разрядности. Еще, для вычисления остатка от деления, потребуется перемножать частное на знаменатель, и полученное число вычитать из числителя. Т.е. потребуется еще подпрограмма вычитания, да и вся эта процедура вычисления остатка тоже будет занимать место и время. Написанная же мною подпрограмма ничего этого не требует. Поэтому, после некоторых колебаний я решил оставить все как есть.
Совсем не обязательно вникать в алгоритм моего способа деления 24-разрядного числа на 16-битное (на самом деле на 15-битное), это довольно опосредованно относится к теме RDA5807m. Поэтому вы можете со спокойной совестью промотать этот раздел, я же чувствую себя обязанным дать необходимые пояснения.
Технически, операцию деления 24-битного числа на 16-битное в идеальном случае можно разбить, на две операции 16-битного деления. Покажу простой пример.
Допустим, нам нужно вычислить день недели для даты 12 июня 2137года. Через онлайн калькулятор мы рассчитываем MJD = 101746, или 0х18D72 в шестнадцатеричной системе. День недели рассчитывается по следующей формуле:
Т.о. нам нужно число 0х18D74 разделить на семь и получить остаток от деления.
Согласно алгоритму деления столбиком, сначала нам нужно выделить из старших разрядов делимого число, которое будет больше знаменателя. Для этого разделим наше число 0х18D74 на два поля, и будем его делить в два этапа по этим полям:
Тогда вычисление будет происходить по следующим формулам. Вычисление частного:
Т.о. мы разбили деление 24-битного числа на две операции деления 16-битных чисел и операции сложения.
Чтобы вычислить остаток от деления, нам нужно во втором слагаемом нашего выражения, вместо деления на семь, вычислить остаток от деления на семь. Т.е. как-то так:
Если зеленую и синюю часть числа 0х18D74 представить как число AB, где A=0x18D*2^8, а B=0x74, то формулы обобщенно можно будет записать так. Вычисления частного:
Вычисление остатка:
Недостатком алгоритма является то, что мы вслепую разделили числа на старшие два байта и младший байт. Если с семеркой в знаменателе алгоритм работает как надо, то если в знаменатель поставить число 0х18D или большее, то алгоритм выдаст некорректный результат. Кроме того, если в результате вычисления числителя второго слагаемого получиться число больше 16-битного, то мы вернемся к тому же, с чего начали, т.е. задаче деления 24-битного числа на 16-битное.
Первую проблему я решаю с помощью сдвига числителя влево, до тех пор, пока в 23-м разряде не появится единица. Затем результат деления сдвигается на тоже количество разрядов вправо. Я специально в формулах разбил константы в знаменателях на числа, которые бы не превышали 215. В таком случае числитель всегда получается больше знаменателя, и он не уходит целиком в остаток. Вторую проблему я решаю с помощью рекурсии. Окончанием рекурсии будет получение 16-битного числителя, который будет делиться одной операцией DIVW.
Если попытаться представить преобразование формулой, то будет наверно как-то так:
Тут нужно пояснить. Во-первых, в программе никаких вычислений степеней двойки не производится, запоминается только число n, т.е. количество разрядов, на которое сдвигается влево исходный числитель. Далее при вычислении множителя, в младший байт числа записывается ноль:
push #0 pushw x ; save quotient in stack |
Т.о. мы расширяем 16-битное число до 24-битного, и одновременно умножаем его на 28. Затем это число сдвигается на n разрядов вправо.
Во-вторых, число B' - это младший байт исходного числа у которого обнулены старшие n-разрядов.
В качестве примера, можно разделить числа 0x11F81A на 0x10AB. Частное от этого деления будет Q=0x0114, остаток R=0x2BE. Число n будет равно 3.
Здесь частое от деления 0x1501A/0x10AB будет =0x14, остаток =0x2BE. Складываем 0х100 + 0х14 и получаем 0x114 в частном, и 0х2BE в остатке.
В программе число 0x1F, на которое производится логическое умножение младшего байта исходного числителя, никак не вычисляется. Вместо этого, младший байт в цикле вначале сдвигается влево на 3 разряда, затем также на 3 разряда вправо. Математически это будет тождественно логическому умножению на 1F.
Для деления 0x1501A/0x10AB нам опять придется вызвать рекурсивно нашу подпрограмму деления, и в этом случае число n будет =7.
В этот раз, в числителе второго множителя выпадает 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 было "быстрое" аппаратное деление, то возможно такой алгоритм и имел бы смысл. Но мне все-равно было интересно реализовать его.
Теперь, когда мы решили все математические проблемы, остается написать подпрограмму, которая будет преобразовывать модифицированную юлианскую дату в года, дни и месяцы. Это довольно рутинный процесс на котором мне не хотелось бы подробно останавливаться, сама подпрограмма подробно прокомментирована. Хочу только сделать одно замечание по алгоритму. Когда я писал и отлаживал данную подпрограмму, я использовал две глобальные 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, восьмерка это число месяца, а пятерка - это месяц май.
Для чтения 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.
Дальнейшей целью нашей работы будет превращение отладочного драйвера в рабочее устройство. Для этого нам предстоит "отвязать" управление драйвером от 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. Файл большой, поэтому я его спрятал под спойлер:
показать код commands.asmstm8/ #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.
Вариантов работы главного цикла не так много. Давайте рассмотрим их.
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. После этого программа переходит на холостой цикл.
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.
Автопоиск - это возможность приемника сканировать радиочастотный диапазон, находить вещающие в нем станции, и запоминать их в EEPROM микроконтролера. Функции автопоиска реализованы в версиях драйвера:
https://gitlab.com/flank1er/stm8_rda5807m/-/tree/master/10_autoscan и https://gitlab.com/flank1er/stm8_rda5807m/-/tree/master/11_eeprom.
Это практически идентичные друг другу версии, но первая версия чисто отладочная. В ней нет функции для сохранения найденных станций в EEPROM, они хранятся в ОЗУ. И это означает, что при каждом включении микроконтроллера приходится заново запускать функцию сканирования. Это удобно с точки зрения отладки, т.к. EEPROM "не дергается" бесчисленными запусками отладчика. Вторая версия драйвера уже полнофункциональная, и сохраняет найденные станции в ППЗУ. Она дополнена подпрограммой записи массива станций в EEPROM, и в программе изменены адреса таблицы станций с RAM на EEPROM. В этой версии, результат сканирования станций записывается в EEPROM, и при старте микроконтроллера первым делом считывается ранее сохраненная таблица станций.
Для реализации функции автопоиска, мне потребовалось к драйверу добавить несколько команд (выделенны красным):
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 * p-/p+ - turn off/turn on preset mode * ps - print table of preset station * scan - start autoscan * 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 * ? - print test message
Команда "scan" запускает автопоиск. Команда "ps" печатает таблицу найденных станций. Команды "p+" и "p-" включают и выключают preset режим, т.е. режим работы с сохраненными станциями. В preset режиме команды "s-", "s+" и "f=num" переключают тюнер между сохраненными станциями, минуя поиск станции.
ВАЖНО: Хочу предупредить, что функцию автопоиска я реализовал только для FM-диапазона: 88-108 МГц. В данном случае номер станции станции занимает один байт. Для 8-битных микроконтроллеров это сокращает программу, что в свою очередь, положительно сказывается на быстродействии.
Кроме добавления новых команд и изменения существующих, изменения были внесены в главный цикл программы, в который была добавлена поддержка режима сканирования/автопоиска. Взаимодействие между главным циклом программы и модулем "commands.asm" осуществляется с помощью флагов переменной RDA_STAT. В данном случае, в нее было добавлено два новых флага - "SCAN" и "PRESET_MODE":
Оба флага устанавливаются и сбрасываются в модуле "commands.asm". Флаг PRESET_MODE никак не влияет на работу главного цикла. Он нужен для того, чтобы команды "s-", "s+", "f=num" переключались на режим работы с таблицей сохраненных станций. Флаг SCAN обеспечивает особый режим работы главного цикла. Кроме холостого режима, главный цикл может работать в режимах: 1) "приема команды", когда выставлется переменная READY и главный цикл парсит эту команду, после чего выполняет ее; 2) режим чтения RDS, когда выставлен флаг RDSREAD; 3) режим автопоиска, когда выставлен флаг SCAN. В этом режиме подавляется печать частот найденных станций, а также происходит цикличный вызов подпрограммы scan_loop. Эта подпрограмма сохраняет найденную станцию, и переходит к поиску следующей. Подробнее об этом будет чуть ниже.
Для нового функционала, в заголовочный файле "rda5807m.inc" были добавлены следующие псевдонимы:
39 SCANBUF cequ {RDSTXT2A+$40} ; adr=0x80 40 SCANID cequ {RDSTXT+8} 41 PRESET cequ {SCANID+2} 42 PRESET_STAT cequ {PRESET+2} 43 PRESET_CODE equ $aa 44 EEPROM equ $4000 45 EEPROM_BUF cequ {EEPROM+2}
Здесь EEPROM - это адрес начала EEPROM. Сюда пишется таблица найденных станций. Формат таблицы станций следующий: заголовок (два байта) + сама таблица. Первый байт заголовка содержит длину таблицы(без заголовка, нумерация с нуля, счет идет в 16-битных словах), которая выражается в количестве станций. Второй байт заголовка содержит хеш-код таблицы. Далее идет сама таблица. Каждая запись в таблице имеет двухбайтную ширину. В первом байте хранится порядковый номер станции в таблице, его нумерация начинается с единицы. Во втором байте хранится частота станции. Т.о. EEPROM_BUF - это адрес начала непосредственно таблицы.
PRESET_CODE - это код истинности таблицы станций в EEPROM. Если в ячейке памяти PRESET хранится значение равное PRESET_CODE, то доступен PRESET режим.
PRESET_STAT - это номер текущей станции в PRESET режиме. Его нумерация идет с единицы.
SCANBUF - это адрес в ОЗУ куда временно пишутся станции в процессе сканирования диапазона. SCANID - это число найденных станций. Нумерация идет с нуля.
Теперь я хочу дать некоторые пояснения к фрагментам программы, которые реализуют функции автопоиска. После того, как драйвер получает команду scan, он вызывает подпрограмму scan из модуля "commands.asm". Она небольшая:
299 scan: 300 #IF USE_UART 301 print_str msg_scan 302 #ENDIF 303 bset RDA_STAT,#SCAN 304 ld a,BAND 305 call set_band 306 clrw x 307 ldw SCANID,x 308 ret
Здесь устанавливается флаг SCAN во флаговой переменной RDA_STAT, обнуляется счетчик SCANID, и вызывается подпрограмма set_band. Она в свою очередь сбрасывает частоту станции, диапазон и шаг станции (spaces) в ноль, после чего вызывает подпрограмму seek_down, и мы начинаем сканирование диапазона "сверху вниз", т.е. от большей частоты к меньшей.
Следующий шаг происходит на стороне главного цикла, когда будет обнаружена станция. А если станция не будет обнаружена, работа тюнера в любом случае завершится по достижению границы диапазона.
loop_delay: #IF WDOG mov IWDG_KR, #$aa ; reset watchdog #ENDIF btjf RDA_STAT,#SCAN, not_scan call scan_loop ldw x,#20 jra to_delay
В главном цикле, при обнаружении установленного флага SCAN во флаговой переменной RDA_STAT, вызывается подпрограмма scan_loop:
309 .scan_loop: 310 ldw y,RDA5807_RDS 311 ld a,yh 312 and a,#03 313 ld yh,a 314 ldw x,SCANID 315 jreq scan_first 316 decw x 317 sllw x 318 cpw y,(SCANBUF,x) 319 jruge scan_quit 320 ldw x,SCANID 321 sllw x 322 scan_first: 323 ldw (SCANBUF,x),y 327 inc {SCANID+1} 328 jp seek_down
Здесь сначала проверяется значение SCANID, и если он равно нулю, то происходит переход по метке "scan_first". Если же SCANID больше нуля, то частота найденной станции сравнивается с предыдущей найденной. Если она больше предыдущей, то происходит выход из цикла. В противном случае станция сохраняется в буфере оперативной памяти, и вновь вызывается seek_down, т.е. происходит переход к следующей итерации цикла.
В случае выхода из цикла "scan_loop", происходит преобразование таблицы найденных станций к рабочему варианту. Так как поиск выполнялся сверху-вниз, станции в таблице идут упорядоченным списком от большей частоты к меньшей. Для пользования приемником, было бы более логичным, если бы станции были отсортированны по возрастанию частоты. Для этого придется "перевернуть" существующий список. Для начала существующую таблицу сохраним в стеке:
329 scan_quit: 330 dec {SCANID+1} 331 ;ld a,{SCANID+1} 332 ld a,#$ff 333 left_loop: 334 inc a 335 x=a 336 sllw x 337 ldw x,(SCANBUF,x) 338 pushw x 339 cp a,{SCANID+1} 340 jrc left_loop
Затем станции извлекаются из стека и записываются снова в таблицу уже в обратном порядке. При этом, каждой станции в старший байт записывается порядковый номер станции.
342 ld a,#$ff 343 right_loop: 344 inc a 345 x=a 346 sllw x 347 ld (SCANBUF,x),a 348 inc (SCANBUF,x) 349 popw y 350 ld yh,a 351 ld a,yl 352 ;incw x 353 ld ({SCANBUF+1},x),a 354 ld a,yh 355 cp a,{SCANID+1} 356 jrc right_loop
Затем сбрасывается SCAN флаг в RDA_STAT, и выводится сообщение, что было найдено столько-то станций:
358 bres RDA_STAT,#SCAN 359 #IF USE_UART 360 print_str msg_found 361 ldw x,SCANID 362 incw x 363 call uart1_print_num 364 print_str msg_stations 365 #ENDIF
После этого идет подсчет хеш-суммы методом "исключающего или":
366 ldw x,SCANID 367 ld a,xl 368 sll a 369 inc a 370 ld xl,a 371 ld a, (SCANBUF,x) 372 pushw x 373 hash_loop: 374 tnzw x 375 jreq hash_quit 376 decw x 377 xor a,(SCANBUF,x) 378 jra hash_loop 379 hash_quit: 380 popw x
По завершении подсчета хеш-суммы вызывается подпрограмма проверки данной хеш-суммы. Если она выполняется успешно, по адресу PRESET должно быть записано записывается значение PRESET_CODE. Если по адресу PRESET какое-то другое значение, то запись таблицы в EEPROM не происходит.
381 incw x 382 ld (SCANBUF,x),a 383 call check_hash 384 ;----------------------------- 385 ld a,PRESET 386 clr PRESET 387 cp a,#PRESET_CODE 388 jrne eeprom_fault
Если все проверки проходят успешно, то идет запись таблицы в EEPROM. Используется обычный однобайтный режим записи:
389 btjt FLASH_IAPSR,#3, eeprom_write ; if DUL flag is set 390 ; da-damm... 391 ldw x,#50 392 call delay 393 ; unlock eeprom 394 mov FLASH_DUKR,#$ae 395 mov FLASH_DUKR,#$56 396 wait_mass_off: 397 btjf FLASH_IAPSR,#3,wait_mass_off 398 eeprom_write: 399 ld a,{SCANID+1} 400 ld EEPROM,a 401 inc a 402 x=a 403 sllw x 404 ld a,(SCANBUF,x) 405 ld {EEPROM+1},a 406 ;mov EEPROM,{SCANID+1} 407 ;mov {EEPROM+1},PRESET 408 ld a,#$ff 409 eeprom_loop: 410 inc a 411 x=a 412 push a 413 sllw x 414 ld a,(SCANBUF,x) 415 ld ({EEPROM+2},x),a 416 incw x 417 ld a,(SCANBUF,x) 418 ld ({EEPROM+2},x),a 419 pop a 420 cp a,{SCANID+1} 421 jrne eeprom_loop 422 ; lock eeprom 423 bres FLASH_IAPSR,#3 424 eeprom_fault: 425 ret
Теперь при старте микроконтроллера, из модуля main, происходит вызов подпрограммы eeprom_check_hash:
call eeprom_check_hash
которая проверяет хеш-сумму таблицы записанной в EEPROM и по результатам проверки выставляет значение в ячейку-памяти PRESET:
1209 .eeprom_check_hash: 1210 ld a,EEPROM 1211 tnz a 1212 jreq eeprom_bad_hash 1213 sll a 1214 inc a 1215 x=a 1216 ld a, (EEPROM_BUF,x) 1217 eeprom_hash_loop2: 1218 tnzw x 1219 jreq eeprom_hash_quit2 1220 decw x 1221 xor a,(EEPROM_BUF,x) 1222 jra eeprom_hash_loop2 1223 eeprom_hash_quit2: 1224 cp a, {EEPROM+1} 1225 jrne eeprom_bad_hash 1226 mov PRESET,#PRESET_CODE 1227 ret 1228 eeprom_bad_hash: 1229 clr PRESET 1230 ret
Таблицу станций записанную в EEPROM можно распечатать по команде "ps":
1239 print_eeprom: 1240 #IF USE_UART 1241 ld a,PRESET 1242 cp a,#PRESET_CODE 1243 jrne quit_print_eeprom 1244 clr a 1245 loop_print_eeprom: 1246 x=a 1247 sllw x 1248 ldw x,(EEPROM_BUF,x) 1249 inc a 1250 call print_station 1251 cp a,EEPROM 1252 jrule loop_print_eeprom 1253 quit_print_eeprom: 1254 #ENDIF 1255 ret
Установка Preset режима происходит по UART-команде "p+", которая вызывает подпрограмму "preset_on". Эта подпрограмма сравнивает текущую частоту с частотами записанными в таблицу, и если находит соответствие, то устанавливает номер станции, в противном случае происходит переключение на ближайшую к текущей частоте станцию.
Разберем по-порядку:
1257 preset_on: 1258 ld a,PRESET 1259 cp a,#PRESET_CODE 1260 jrne quit_preset 1261 #IF USE_UART 1262 print_str msg_preset_on 1263 #ENDIF 1264 bset RDA_STAT,#PRESET_MODE
Здесь вначале проверяется ячейка памяти PRESET. Если ее содержимое не равно PRESET_CODE, то с таблицей станций что-то не так, скорее всего ее попросту нет. Следовательно, Preset режим не доступен, выходим из подпрограммы. Если же с PRESET_CODE все в порядке, печатаем сообщение, и устанавливаем флаг PRESET_MODE.
Затем вызываем подпрограмму rda5807m_get_readchan. В регистре Х она возвращает текущую частоту:
1265 call rda5807m_get_readchan
Помещаем ее в стек, и начинаем в цикле сравнивать табличные значения частот и текущую частоту:
1266 pushw x 1267 ld a,#$ff 1268 preset_loop: 1269 inc a 1270 cp a,EEPROM 1271 jrugt last_station 1272 x=a 1273 sllw x 1274 ldw x,(EEPROM_BUF,x) 1275 push a 1276 ld a,xh 1277 ld PRESET_STAT,a 1278 ld a,xl 1279 cp a,(3,sp) 1280 pop a 1281 jrc preset_loop
Цикл крутится пока табличная частота меньше текущей. Как только она становится больше или равной, происходит выход из цикла. Если счетчик цикла превышает количество станций (случается когда текущая частота больше частоты последней станции в таблице), то происходит переход по метке "last_station". В этом случае устанавливается последняя частота из таблицы.
Далее, если частоты из таблицы и текущая не равны друг другу, то происходит перенастройка тюнера на частоту из таблицы.
1282 cpw x,(1,sp) 1283 jreq if_was_found 1284 addw sp,#2 1285 clr a 1286 ld xh,a 1287 ld a,SPACE 1288 push a 1289 jp space_recalc
В случае, если частоты все же равны, то выводится сообщение и затем следует выход из подпрограммы:
1290 if_was_found: 1291 #IF USE_UART 1292 call print_station 1293 #ENDIF 1294 popw x 1295 quit_preset: 1296 ret
Подпрограмма выхода из Preset режима, которая вызывается UART-командой "p-" выглядит так:
1232 preset_off: 1233 #IF USE_UART 1234 print_str msg_preset_off 1235 #ENDIF 1236 bres RDA_STAT,#PRESET_MODE 1237 ret
Здесь мы выводим сообщение и сбрасываем флаг PRESET_MODE.
Напоследок осталось научить команды: "s-", "s+", "f=num" работать с Preset режимом. Начнем с команды "s-":
428 seek_down: 429 btjf RDA_STAT,#PRESET_MODE,seek_down_no_preset 430 ld a,PRESET 431 cp a,#PRESET_CODE 432 jrne seek_down_no_preset 433 ld a,PRESET_STAT 434 cp a,#1 435 jrne seek_down_preset_not_zero 436 jp preset4
Здесь в начале у нас идут проверки: установлен ли Preset-режим, корректна ли таблица станций, не является ли текущая станция первой? В последнем случае снова идет переход подпрограмму "last_station", не смотря на то, что написано "preset4":
1297 last_station: 1298 addw sp,#2 1299 preset4:
Если все проверки прошли успешно, то текущая станция в PRESET_STAT уменьшается на единицу, далее через подпрограмму "get_preset_stat" считывается из таблицы частота станции, после чего идет переход по метке "space_recalc", где тюнер перенастраивается на новую частоту:
437 seek_down_preset_not_zero: 438 dec a 439 ld PRESET_STAT,a 440 dec a 441 call get_preset_stat 442 clr a 443 ld xh,a 444 ld a,SPACE 445 push a 446 jp space_recalc
Аналогичным образом работает команда "s+":
643 seek_up: 644 btjf RDA_STAT,#PRESET_MODE,seek_up_no_preset 645 ld a,PRESET 646 cp a,#PRESET_CODE 647 jrne seek_up_no_preset 648 ld a,EEPROM 649 inc a 650 cp a,PRESET_STAT 651 jrne seek_up_preset_not_zero 652 clr a 653 jra seek_up_zero 654 seek_up_preset_not_zero: 655 ld a,PRESET_STAT 656 inc PRESET_STAT 657 call get_preset_stat 658 clr a 659 ld xh,a 660 ld a,SPACE 661 push a 662 jp space_recalc 663 seek_up_zero 664 call get_preset_stat 665 clr a 666 ld xh,a 667 ld PRESET_STAT,a 668 inc PRESET_STAT 669 ld a,SPACE 670 push a 671 jp space_recalc
Команда "f=num" действует похожим образом. В качестве num указывается номер станции из таблицы. В начале идут проверки на Preset режим, корректность таблицы, и на то, чтобы число num не превышало количество станций в таблице:
687 freq: 688 ; CHECK: if was recived "f=NUM.NUM" command 689 ldw x,#02 690 call strnum ; get integer part 691 y=a ; y = integer part 692 ;=========================================== 693 btjf RDA_STAT,#PRESET_MODE,freq_no_preset 694 ld a,PRESET 695 cp a,#PRESET_CODE 696 jrne freq_no_preset 697 ld a,yl 698 tnz a 699 jreq freq_invalid 700 dec a 701 cp a,EEPROM 702 jrugt freq_invalid
Если все проверки пройдены, с помощью подпрограммы "get_preset_stat" из таблицы извлекается частота заданной станции, и далее тюнер перенастраивается на заданную частоту:
703 ld PRESET_STAT,a 704 inc PRESET_STAT 705 call get_preset_stat 706 clr a 707 ld xh,a 708 ld a,SPACE 709 push a 710 jp space_recalc 711 freq_invalid: 712 print_str msg_invalid 713 ret
На этом я заканчиваю обзор кода отвечающего за реализацию автопоиска в драйвере RDA5807m. Обзор производился по версии драйвера https://gitlab.com/flank1er/stm8_rda5807m/-/tree/master/11_eeprom. Номера строк модуля commands.asm могут незначительно отличаться.
Еще один важный момент. Когда вы слушаете какую-либо станцию, непосредственно подключив наушники к RDA5807, и если при этом включите прием RDS сообщений командой "r+" или "l+", то в наушниках услышите монотонный шум который создает I2C шина. Чтобы от него избавиться, нужно подключить конденсаторы на 4.7мкФ о которых я писал ранее в: 14) Работа над ошибками (борьба с аппаратным багом программными средствами).
В следующий раз речь пойдет о подключении к драйверу энкодера и 4-х разрядного семисегментного индикатора.