В микроконтроллерах PIC16 отсутствует аппаратный блок умножения и деления чисел, но эти арифметические операции можно реализовать программным путем.
Операцию умножения можно представить в виде многократного сложения, деление – многократным вычитанием. Например, выражение 8х3=24 равнозначно 8+8+8=24, для деления в качестве примера возьмем следующее выражение 36:10=3,6. Алгоритм деления можно представить так: 36-10-10-10-10=-4, то есть вычитаем из 36 число 10 до тех пор, пока не получим отрицательный результат. При этом результатом операции деления является количество вычитаний, но только без учета последнего вычитания, которое привело к отрицательному результату, так как я рассматриваю здесь только целочисленные операции без дробных частей. В данном примере после четырех вычитаний получаем -4, соответственно ответ равен 3, без учета последнего вычитания.
Рассмотрим подпрограмму умножения однобайтных чисел, перед вызовом подпрограммы загружаем числа в регистры varLL и tmpLL (число в varLL умножается на число в tmpLL), в подпрограмме первым делом очищаем регистр varLH, так как максимальный результат при перемножении однобайтных чисел равен 65025, соответственно результат это двухбайтное число (varLH, varLL). Далее проверяем перемножаемые числа на равенство нулю, если число в регистре varLL равно нулю, то выходим из подпрограммы с нулевым результатом, при обнаружении нуля в tmpLL, очищаем регистр varLL, также получая нулевой результат. Если ни одно из чисел не равно нулю копируем число, содержащееся в регистре varLL в дополнительный регистр regLL, это число будет выступать в качестве константы для дальнейшего многократного сложения. Регистр tmpLL будет выступать в качестве счетчика сложений, перед каждой операцией сложения (прибавляем однобайтное число из регистра regLL к двухбайтному числу в регистрах varLH, varLL – это операция сложения многобайтных чисел, о которой я писал в статье про сложение и вычитание) производим декремент регистра tmpLL. После обнуления регистра выходим из подпрограммы, умножение закончено.
Остальные подпрограммы, по сути, ничем не отличаются, составлены по такому же алгоритму, разница лишь в применении различных вариантов сложения многобайтных чисел. Ниже представлены коды подпрограмм:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
;Подпрограмма умножения однобайтных чисел (varLL)x(tmpLL) ;Первое число предварительно загружается в регистр varLL ;Второе число предварительно загружается в регистр tmpLL ;Результат умножения в регистре varHL, varLL, максимальный резултат ;составит 255х255=65025 (двухбайтное число) ;в подпрограмме используется дополнительный регистр regLL, для хранения константы axb clrf varLH ;очистка регистра varLH (эквивалентно записи нуля) movlw .0 ;побитное исключающее "или" числа ноль и числа лежащего в xorwf varLL,W ;регистре varLL: это проверка равенства нулю числа лежащего btfsc STATUS,Z ;в регистре varLL return ;число в регистре varLL равно нулю: выход из подпрограммы movlw .0 ;число в регистре varLL не равно нулю: проверка равенства xorwf tmpLL,W ;нулю числа лежащего в регистре tmpLL btfsc STATUS,Z ; goto a1 ;число в регистре tmpLL равно нулю: переход на метку a1 movf varLL,W ;число в tmpLL не равно нулю: копирование числа из varLL movwf regLL ;в регистр regLL, в качестве константы для дальнейшего сложения a3 decfsz tmpLL,F ;декремент (с условием) регистра tmpLL, регистр tmpLL ;выступает в качестве счетчика сложений goto a2 ;регистр tmpLL не равен нулю: переход на метку a2 return ;регистр tmpLL равен нулю: выход из подпрограммы a2 movf regLL,W ;прибавление числа из регистра regLL к числу addwf varLL,F ;в регистре varLL btfsc STATUS,C ;проверка переполнения регистра varLL incf varLH,F ;переполнение varLL: инкремент регистра varLH goto a3 ;переход на метку a3 a1 clrf varLL ;очистка регистра varLL (эквивалентно записи нуля) return ;выход из подпрограммы ; ;Подпрограмма умножения двухбайтного и однобайтного числа (varLH, varLL)x(tmpLL) ;Двухбайтное число предварительно загружается в регистры varLH (старший байт) и varLL (младший байт) ;Однобайтное число предварительно загружается в регистр tmpLL ;Результат умножения в регистрах varHL, varLH, varLL, максимальный резултат ;составит 65535х255=16 711 425 (трехбайтное число) ;в подпрограмме используются дополнительные регистры (regLL, regLH) для хранения константы axb clrf varHL ;очистка регистра varHL (эквивалентно записи нуля) movlw .0 ;побитное исключающее "или" числа ноль и числа лежащего в xorwf varLL,W ;регистре varLL: это проверка равенства нулю числа лежащего btfss STATUS,Z ;в регистре varLL goto a1 ;число в регистре varLL не равно нулю: переход на метку a1 movlw .0 ;число в регистре varLL равно нулю: проверка равенства xorwf varLH,W ;нулю числа лежащего в регистре varLH btfsc STATUS,Z ; return ;число в регистре varLH равно нулю: выход из подпрограммы a1 movlw .0 ;число в регистре varLH не равно нулю: проверка равенства xorwf tmpLL,W ;нулю числа лежащего в регистре tmpLL btfsc STATUS,Z ; goto a2 ;число в регистре tmpLL равно нулю: переход на метку a2 movf varLL,W ;число в регистре tmpLL не равно нулю: копирование чисел из movwf regLL ;из регистров varLL, varLH в регистры regLL, regLH в movf varLH,W ;качечтве константы для дальнейшего сложения movwf regLH a4 decfsz tmpLL,F ;декремент (с условием) регистра tmpLL, регистр tmpLL ;выступает в качестве счетчика сложений goto a3 ;регистр tmpLL не равен нулю: переход на метку a3 return ;регистр tmpLL равен нулю: выход из подпрограммы ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; a3 movf regLL,W ; addwf varLL,F ; btfss STATUS,C ; goto a5 ; incfsz varLH,F ;прибавление двухбайтного числа (regLH, regLL) к трехбайтному goto a5 ;числу в регистрах varHL, varLH, varLL, то есть это операция incf varHL,F ;сложения трехбайтного и двухбайтного числа a5 movf regLH,W ; addwf varLH,F ; btfsc STATUS,C ; incf varHL,F ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; goto a4 ;переход на метку a4 a2 clrf varLL ;очистка регистров varLL, varLH (эквивалентно записи нуля) clrf varLH return ;выход из подпрограммы ; ;Подпрограмма умножения двухбайтных чисел (varLH, varLL)x(tmpLH, tmpLL) ;Первое число предварительно загружается в регистры varLH, varLL ;Второе число предварительно загружается в регистр tmpLH, tmpLL ;Результат умножения в регистрах varHH, varHL, varLH, varLL, максимальный резултат ;составит 65535х65535=4 294 836 225 (четырехбайтное число) ;в подпрограмме используются дополнительные регистры (regLL, regLH) для хранения константы axb clrf varHL ;очистка регистров varHL, varHH (эквивалентно записи нуля) clrf varHH ; movlw .0 ;побитное исключающее "или" числа ноль и числа лежащего в xorwf varLL,W ;регистре varLL: это проверка равенства нулю числа лежащего btfss STATUS,Z ;в регистре varLL goto a1 ;число в регистре varLL не равно нулю: переход на метку a1 movlw .0 ;число в регистре varLL равно нулю: проверка равенства xorwf varLH,W ;нулю числа лежащего в регистре varLH btfsc STATUS,Z ; return ;число в регистре varLH равно нулю: выход из подпрограммы a1 movlw .0 ;число в регистре varLH не равно нулю: проверка равенства xorwf tmpLL,W ;нулю числа лежащего в регистре tmpLL btfss STATUS,Z ; goto a6 ;число в регистре tmpLL не равно нулю: переход на метку a6 movlw .0 ;число в регистре varLH равно нулю: проверка равенства xorwf tmpLH,W ;нулю числа лежащего в регистре tmpLH btfsc STATUS,Z ; goto a2 ;число в регистре tmpLH равно нулю: переход на метку a2 a6 movf varLL,W ;число в регистре tmpLH не равно нулю: копирование чисел из movwf regLL ;из регистров varLL, varLH в регистры regLL, regLH в movf varLH,W ;качестве константы для дальнейшего сложения movwf regLH ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; a4 movlw .1 ; subwf tmpLL,F ; btfsc STATUS,C ; goto a7 ; decf tmpLH,F ;декремент двухбайтного числа в регистрах tmpLL, tmpLH a7 movlw .0 ;Регистры tmpLL, tmpLH выступают в качестве счетчика сложений xorwf tmpLL,W ; btfss STATUS,Z ;если значения регистров tmpLL, tmpLH не равны нулю то goto a3 ;переходим на метку a3, если равны нулю выходим из подпрограммы movlw .0 ; xorwf tmpLH,W ; btfsc STATUS,Z ; return ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; a3 movf regLL,W ; addwf varLL,F ; btfss STATUS,C ; goto a5 ; incfsz varLH,F ;прибавление двухбайтного числа (regLH, regLL) к четырехбайтному goto a5 ;числу в регистрах varHH, varHL, varLH, varLL, то есть это incfsz varHL,F ;операция сложения четырехбайтного и двухбайтного числа goto a5 ; incf varHH,F ; a5 movf regLH,W ; addwf varLH,F ; btfss STATUS,C ; goto a4 ; incfsz varHL,F ; goto a4 ; incf varHH,F ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; goto a4 ;переход на метку a4 a2 clrf varLL ;очистка регистров varLL, varLH (эквивалентно записи нуля) clrf varLH ; return ;выход из подпрограммы ; |
Теперь рассмотрим деление однобайтных чисел. Также предварительно загружаем числа в регистры varLL и tmpLL, (число в varLL делится на число в tmpLL). Очищаем регистр результата rezLL, проверяем число в регистре tmpLL, если оно равно нулю, то выходим из подпрограммы без изменений, так как делить на ноль нельзя. Далее выполняем вычитание числа лежащего в tmpLL из числа в регистре varLL, при положительном результате инкрементируем регистр rezLL. При отрицательном результате выходим из подпрограммы, деление завершено.
Деление многобайтных чисел производится по тому же алгоритму, коды подпрограмм представлены ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
;Подпрограмма деления однобайтных чисел (varLL):(tmpLL) ;Первое число предварительно загружается в регистр varLL ;Второе число предварительно загружается в регистр tmpLL ;Результат деления в регистре rezLL, деление целочисленное без дробной части ;на ноль делить нельзя, произойдет выход из подпрограммы без изменений del clrf rezLL ;очистка регистра rezLL (эквивалентно записи нуля) movlw .0 ;проверка равенства нулю числа лежащего в регистре tmpLL xorwf tmpLL,W ;(на ноль делить нельзя) btfsc STATUS,Z ; return ;число в регистре tmpLL равно нулю: выход из подпрограммы d1 movf tmpLL,W ;число в tmpLL не равно нулю: вычитаем число лежащее в регистре subwf varLL,F ;tmpLL из числа в регистре varLL btfss STATUS,C ;проверка на факт заема return ;возник факт заема: выход из подпрограммы incf rezLL,F ;нет заема: инкремент регистра rezLL (регистр rezLL выступает в ;качестве счетчика вычитания и содержит результат деления) goto d1 ;переход на метку d1 для повторного вычитания, операция деления ;представляет собой многократное вычитание ; ;Подпрограмма деления двухбайтного числа на однобайтное (varLH, varLL):(tmpLL) ;Двухбайтное число предварительно загружается в регистр varLH, varLL ;Однобайтное число предварительно загружается в регистр tmpLL ;Результат деления в регистре rezLH, rezLL, деление целочисленное без дробной части ;на ноль делить нельзя, произойдет выход из подпрограммы без изменений del clrf rezLL ;очистка регистров rezLL, rezLH (эквивалентно записи нуля) clrf rezLH ; movlw .0 ;проверка равенства нулю числа лежащего в регистре tmpLL xorwf tmpLL,W ;(на ноль делить нельзя) btfsc STATUS,Z ; return ;число в регистре tmpLL равно нулю: выход из подпрограммы ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; d1 movf tmpLL,W ;число в tmpLL не равно нулю: вычитаем число лежащее в регистре subwf varLL,F ;tmpLL из числа в регистрах varLH, varLL: это операция вычитания btfsc STATUS,C ;однобайтного числа из двухбайтного goto d2 ;при отрицательном результате происходит выход из подпрограммы movlw .1 ;при положительном результате инкрементируем счетчик вычитаний subwf varLH,F ;переходя на метку d2 btfss STATUS,C ; return ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; d2 incfsz rezLL,F ;инкремент rezLL с проверкой на переполнение goto d1 ;нет переполнения rezLL: переход на метку d1 incf rezLH,F ;переполнение rezLL: инкремент регистра rezLH (регистры rezLL, rezLH ;выступают в качестве счетчика вычитания и содержат результат деления) goto d1 ;переход на метку d1 для повторного вычитания, операция деления ;представляет собой многократное вычитание ; ;Подпрограмма деления двухбайтных чисел (varLH, varLL):(tmpLL, tmpLH) ;Первое число предварительно загружается в регистр varLH, varLL ;Второе число предварительно загружается в регистр tmpLH, tmpLL ;Результат деления в регистре rezLH, rezLL, деление целочисленное без дробной части ;на ноль делить нельзя, произойдет выход из подпрограммы без изменений del clrf rezLL ;очистка регистров rezLL, rezLH (эквивалентно записи нуля) clrf rezLH ; movlw .0 ;проверка равенства нулю числа лежащего в регистре tmpLL xorwf tmpLL,W ; btfss STATUS,Z ; goto d1 ;число в регистре tmpLL не равно нулю: переход на метку d1 movlw .0 ;число в регистре tmpLL равно нулю: проверка равенства нулю xorwf tmpLH,W ;числа лежащего в регистре tmpLH btfsc STATUS,Z ; return ;число в регистре tmpLH равно нулю: выход из подпрограммы ;(на ноль делить нельзя) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; d1 movf tmpLL,W ;число в tmpLH не равно нулю: вычитаем число лежащее в регистрах subwf varLL,F ;tmpLH, tmpLL из числа в регистрах varLH, varLL: это операция btfsc STATUS,C ;вычитания двухбайтных чисел goto d2 ;при отрицательном результате происходит выход из подпрограммы movlw .1 ;при положительном результате инкрементируем счетчик вычитаний subwf varLH,F ;rezLL, rezLH btfss STATUS,C ; return ; d2 movf tmpLH,W ; subwf varLH,F ; btfss STATUS,C ; return ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; incfsz rezLL,F ;инкремент rezLL с проверкой на переполнение goto d1 ;нет переполнения rezLL: переход на метку d1 incf rezLH,F ;переполнение rezLL: инкремент регистра rezLH (регистры rezLL, rezLH ;выступают в качестве счетчика вычитания и содержат результат деления) goto d1 ;переход на метку d1 для повторного вычитания, операция деления ;представляет собой многократное вычитание ; |
Следует знать, что в отличие от подпрограмм сложения и вычитания, умножение и деление могут занять значительное процессорное время. Умножение чисел 255х255=65025 занимает около 2303 машинных циклов, при частоте тактового генератора в 20 Мгц, время расчета составит 460 мкс. В основном время расчета зависит от числа, на которое умножаем (прямая зависимость). При делении продолжительность расчета увеличивается с уменьшением числа, на которое делим, например операция деления чисел 255:1=255 займет около 1797 машинных циклов, при 20 МГц время расчета составит 359,4 мкс.
Деление и умножение на число 2 можно производить с помощью сдвига содержимого регистра вправо и влево соответственно, с помощью команд RRF и RLF. Сдвиг происходит через флаг C регистра STATUS, при сдвиге вправо, младший бит передается в C, а значение бита C передается в старший бит сдвигаемого регистра, при сдвиге влево все наоборот. Поэтому при применении команд сдвига для деления и умножения необходимо предварительно очистить бит C регистра STATUS.
Понятные рабочие алгоритмы замечательно и аккуратно выполнены … Автору – интересных заказов от достойных клиентов!
В конце статьи упомянуты команды RRF и RLF, а ведь с их помощью приращение\уменьшение в регистрах с конечным результатом будет на много быстрее и для этого будет возможен выигрыш в количестве строк кода и времени выполнения? Или я ошибаюсь?
Да все верно, можно оптимизировать команды умножения и деления для более быстрого расчета, здесь я не рассмотрел эти варианты.
Спасибо за отличный сайт!
Было бы здорово, если бы в этой статье привели код деления трёхбайтного числа на двухбайтное. Это очень актуально для 8-битных микропроцессоров.
Также, было бы интересно увидеть пример двойного повышения точности в коде деления двух двухбайтных чисел. Для этого потребуется ещё один доп. разряд и ещё один регистр оперативной памяти, но оно того стоит.
Переписал Ваши программы умножения и деления чисел. Протестировал их в симуляторе MPLAB. Подсчеты идут правильно. Результаты совпадают с результатами подсчитанными на калькуляторе. Но у меня длительности подсчета в машинных циклах отличаются от Ваших. Например Вы пишите: “Умножение чисел 255х255=65025 занимает около 2303 машинных циклов” У меня же получилось ровно 2391 м.ц. И так во всех примерах длительность получается несколько бОльшей чем у Вас. Мой контроллер PIC16F628A, тактовая частота установленна 4МГц. Насколько я понимаю, от этой частоты зависит длительность выполнения вычесленний в микросекундах, но не в м.ц.
Еще одно наблюдение. При делении максимально возможного двухбайтного числа 65535 на 1 вычесление результата, который виден глазом и без вычеслений занимает 823887 м.ц. Это при частоте микроконтроллера 4МГц составляет после округления 0,824 сек. Т.е. почти секунда! Если не использовать специальные меры по организации многозадачности, например RTOS, то устройство зависнет на секунду, или его сбросит сторожевой таймер, не дав досчитать до конца. Теперь понятно почему Вы не стали создавать код деления 3-х и 4-х байтных чисел.
Просто, любопытства ради, в подпрограмму умножения двухбайтных чисел запустил две пары с максимально возможным значением, частоту процессора установил 32768 Гц (некоторые товарищи любят юзать свои микроконтроллеры на такой частоте) и любезно попросил симулятор посчитать мне произведение. Что вы думаете? Результат пришел через 200,907837 сек. 200 с лишним секунд! Я думаю, калькулятор созданный по такому принципу не будет пользоваться популярностью даже в среде школьников.
В статье приведены самые простые по логике подпрограммы, да они затрачивают много времени, для лучшего быстродействия нужно использовать другие подпрограммы деления и умножения.
В умножении байтов в подпрограмме и в описании одна и та же переменная обозвана varHL и varLH. На работе не сказывется, но всё же непорядок.